diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp index a7a027cb38..4fc93ce7a2 100644 --- a/staticlibs/Android.bp +++ b/staticlibs/Android.bp @@ -38,6 +38,7 @@ java_library { "device/com/android/net/module/util/FdEventsReader.java", "device/com/android/net/module/util/HexDump.java", "device/com/android/net/module/util/PacketReader.java", + "device/com/android/net/module/util/SharedLog.java", // This library is used by system modules, for which the system health impact of Kotlin // has not yet been evaluated. Annotations may need jarjar'ing. // "src_devicecommon/**/*.kt", @@ -95,7 +96,7 @@ filegroup { java_library { name: "net-utils-device-common-netlink", - // TODO: Ipv6Utils and Struct stuff could be separated out of th netlink library into + // TODO: Ipv6Utils and Struct stuff could be separated out of the netlink library into // an individual Struct library, and remove the net-utils-framework-common lib dependency. // But there is no need doing this at the moment. srcs: [ @@ -124,6 +125,36 @@ java_library { ], } +java_library { + // TODO : this target should probably be folded into net-utils-device-common + name: "net-utils-device-common-ip", + srcs: [ + "device/com/android/net/module/util/ip/*.java", + ], + sdk_version: "module_current", + min_sdk_version: "29", + visibility: [ + "//frameworks/libs/net/common/tests:__subpackages__", + "//frameworks/libs/net/common/testutils:__subpackages__", + "//packages/modules/Connectivity:__subpackages__", + "//packages/modules/NetworkStack:__subpackages__", + ], + libs: [ + "framework-annotations-lib", + "framework-connectivity", + ], + static_libs: [ + "net-utils-device-common", + "net-utils-device-common-netlink", + "net-utils-framework-common", + "netd-client", + ], + apex_available: [ + "com.android.tethering", + "//apex_available:platform", + ], +} + java_library { name: "net-utils-framework-common", srcs: [ @@ -205,7 +236,11 @@ filegroup { filegroup { name: "net-utils-framework-wifi-common-srcs", srcs: [ - "framework/com/android/net/module/util/**/*.java", + "framework/com/android/net/module/util/DnsSdTxtRecord.java", + "framework/com/android/net/module/util/Inet4AddressUtils.java", + "framework/com/android/net/module/util/InetAddressUtils.java", + "framework/com/android/net/module/util/MacAddressUtils.java", + "framework/com/android/net/module/util/NetUtils.java", ], path: "framework", visibility: [ diff --git a/staticlibs/device/com/android/net/module/util/SharedLog.java b/staticlibs/device/com/android/net/module/util/SharedLog.java new file mode 100644 index 0000000000..37c6f6dc23 --- /dev/null +++ b/staticlibs/device/com/android/net/module/util/SharedLog.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.net.module.util; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.text.TextUtils; +import android.util.Log; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.time.LocalDateTime; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.StringJoiner; + + +/** + * Class to centralize logging functionality for tethering. + * + * All access to class methods other than dump() must be on the same thread. + * + * @hide + */ +public class SharedLog { + private static final int DEFAULT_MAX_RECORDS = 500; + private static final String COMPONENT_DELIMITER = "."; + + private enum Category { + NONE, + ERROR, + MARK, + WARN, + } + + private final LocalLog mLocalLog; + // The tag to use for output to the system log. This is not output to the + // LocalLog because that would be redundant. + private final String mTag; + // The component (or subcomponent) of a system that is sharing this log. + // This can grow in depth if components call forSubComponent() to obtain + // their SharedLog instance. The tag is not included in the component for + // brevity. + private final String mComponent; + + public SharedLog(String tag) { + this(DEFAULT_MAX_RECORDS, tag); + } + + public SharedLog(int maxRecords, String tag) { + this(new LocalLog(maxRecords), tag, tag); + } + + private SharedLog(LocalLog localLog, String tag, String component) { + mLocalLog = localLog; + mTag = tag; + mComponent = component; + } + + public String getTag() { + return mTag; + } + + /** + * Create a SharedLog based on this log with an additional component prefix on each logged line. + */ + public SharedLog forSubComponent(String component) { + if (!isRootLogInstance()) { + component = mComponent + COMPONENT_DELIMITER + component; + } + return new SharedLog(mLocalLog, mTag, component); + } + + /** + * Dump the contents of this log. + * + *
This method may be called on any thread. + */ + public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { + mLocalLog.dump(writer); + } + + ////// + // Methods that both log an entry and emit it to the system log. + ////// + + /** + * Log an error due to an exception. This does not include the exception stacktrace. + * + *
The log entry will be also added to the system log. + * @see #e(String, Throwable) + */ + public void e(Exception e) { + Log.e(mTag, record(Category.ERROR, e.toString())); + } + + /** + * Log an error message. + * + *
The log entry will be also added to the system log. + */ + public void e(String msg) { + Log.e(mTag, record(Category.ERROR, msg)); + } + + /** + * Log an error due to an exception, with the exception stacktrace if provided. + * + *
The error and exception message appear in the shared log, but the stacktrace is only + * logged in general log output (logcat). The log entry will be also added to the system log. + */ + public void e(@NonNull String msg, @Nullable Throwable exception) { + if (exception == null) { + e(msg); + return; + } + Log.e(mTag, record(Category.ERROR, msg + ": " + exception.getMessage()), exception); + } + + /** + * Log an informational message. + * + *
The log entry will be also added to the system log. + */ + public void i(String msg) { + Log.i(mTag, record(Category.NONE, msg)); + } + + /** + * Log a warning message. + * + *
The log entry will be also added to the system log. + */ + public void w(String msg) { + Log.w(mTag, record(Category.WARN, msg)); + } + + ////// + // Methods that only log an entry (and do NOT emit to the system log). + ////// + + /** + * Log a general message to be only included in the in-memory log. + * + *
The log entry will *not* be added to the system log. + */ + public void log(String msg) { + record(Category.NONE, msg); + } + + /** + * Log a general, formatted message to be only included in the in-memory log. + * + *
The log entry will *not* be added to the system log. + * @see String#format(String, Object...) + */ + public void logf(String fmt, Object... args) { + log(String.format(fmt, args)); + } + + /** + * Log a message with MARK level. + * + *
The log entry will *not* be added to the system log.
+ */
+ public void mark(String msg) {
+ record(Category.MARK, msg);
+ }
+
+ private String record(Category category, String msg) {
+ final String entry = logLine(category, msg);
+ mLocalLog.append(entry);
+ return entry;
+ }
+
+ private String logLine(Category category, String msg) {
+ final StringJoiner sj = new StringJoiner(" ");
+ if (!isRootLogInstance()) sj.add("[" + mComponent + "]");
+ if (category != Category.NONE) sj.add(category.toString());
+ return sj.add(msg).toString();
+ }
+
+ // Check whether this SharedLog instance is nominally the top level in
+ // a potential hierarchy of shared logs (the root of a tree),
+ // or is a subcomponent within the hierarchy.
+ private boolean isRootLogInstance() {
+ return TextUtils.isEmpty(mComponent) || mComponent.equals(mTag);
+ }
+
+ private static final class LocalLog {
+ private final Deque IPv6 should be disabled before changing the mode.
+ */
+ public boolean setIPv6AddrGenModeIfSupported(int mode) {
+ try {
+ mNetd.setIPv6AddrGenMode(mIfName, mode);
+ } catch (RemoteException e) {
+ logError("Unable to set IPv6 addrgen mode: %s", e);
+ return false;
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode != OsConstants.EOPNOTSUPP) {
+ logError("Unable to set IPv6 addrgen mode: %s", e);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Add an address to the interface.
+ */
+ public boolean addAddress(LinkAddress addr) {
+ return addAddress(addr.getAddress(), addr.getPrefixLength());
+ }
+
+ /**
+ * Add an address to the interface.
+ */
+ public boolean addAddress(InetAddress ip, int prefixLen) {
+ try {
+ mNetd.interfaceAddAddress(mIfName, ip.getHostAddress(), prefixLen);
+ } catch (ServiceSpecificException | RemoteException e) {
+ logError("failed to add %s/%d: %s", ip, prefixLen, e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Remove an address from the interface.
+ */
+ public boolean removeAddress(InetAddress ip, int prefixLen) {
+ try {
+ mNetd.interfaceDelAddress(mIfName, ip.getHostAddress(), prefixLen);
+ } catch (ServiceSpecificException | RemoteException e) {
+ logError("failed to remove %s/%d: %s", ip, prefixLen, e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Remove all addresses from the interface.
+ */
+ public boolean clearAllAddresses() {
+ try {
+ mNetd.interfaceClearAddrs(mIfName);
+ } catch (Exception e) {
+ logError("Failed to clear addresses: %s", e);
+ return false;
+ }
+ return true;
+ }
+
+ private void logError(String fmt, Object... args) {
+ mLog.e(String.format(fmt, args));
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/ip/IpNeighborMonitor.java b/staticlibs/device/com/android/net/module/util/ip/IpNeighborMonitor.java
new file mode 100644
index 0000000000..88f8c9d37d
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/ip/IpNeighborMonitor.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.ip;
+
+import static android.system.OsConstants.NETLINK_ROUTE;
+
+import static com.android.net.module.util.netlink.NetlinkConstants.RTM_DELNEIGH;
+import static com.android.net.module.util.netlink.NetlinkConstants.hexify;
+import static com.android.net.module.util.netlink.NetlinkConstants.stringForNlMsgType;
+
+import android.net.MacAddress;
+import android.os.Handler;
+import android.system.ErrnoException;
+import android.system.OsConstants;
+import android.util.Log;
+
+import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.netlink.NetlinkMessage;
+import com.android.net.module.util.netlink.NetlinkSocket;
+import com.android.net.module.util.netlink.RtNetlinkNeighborMessage;
+import com.android.net.module.util.netlink.StructNdMsg;
+
+import java.net.InetAddress;
+import java.util.StringJoiner;
+
+/**
+ * IpNeighborMonitor.
+ *
+ * Monitors the kernel rtnetlink neighbor notifications and presents to callers
+ * NeighborEvents describing each event. Callers can provide a consumer instance
+ * to both filter (e.g. by interface index and IP address) and handle the
+ * generated NeighborEvents.
+ *
+ * @hide
+ */
+public class IpNeighborMonitor extends NetlinkMonitor {
+ private static final String TAG = IpNeighborMonitor.class.getSimpleName();
+ private static final boolean DBG = false;
+ private static final boolean VDBG = false;
+
+ /**
+ * Make the kernel perform neighbor reachability detection (IPv4 ARP or IPv6 ND)
+ * for the given IP address on the specified interface index.
+ *
+ * @return 0 if the request was successfully passed to the kernel; otherwise return
+ * a non-zero error code.
+ */
+ public static int startKernelNeighborProbe(int ifIndex, InetAddress ip) {
+ final String msgSnippet = "probing ip=" + ip.getHostAddress() + "%" + ifIndex;
+ if (DBG) Log.d(TAG, msgSnippet);
+
+ final byte[] msg = RtNetlinkNeighborMessage.newNewNeighborMessage(
+ 1, ip, StructNdMsg.NUD_PROBE, ifIndex, null);
+
+ try {
+ NetlinkSocket.sendOneShotKernelMessage(NETLINK_ROUTE, msg);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Error " + msgSnippet + ": " + e);
+ return -e.errno;
+ }
+
+ return 0;
+ }
+
+ /**
+ * An event about a neighbor.
+ */
+ public static class NeighborEvent {
+ final long elapsedMs;
+ final short msgType;
+ final int ifindex;
+ final InetAddress ip;
+ final short nudState;
+ final MacAddress macAddr;
+
+ public NeighborEvent(long elapsedMs, short msgType, int ifindex, InetAddress ip,
+ short nudState, MacAddress macAddr) {
+ this.elapsedMs = elapsedMs;
+ this.msgType = msgType;
+ this.ifindex = ifindex;
+ this.ip = ip;
+ this.nudState = nudState;
+ this.macAddr = macAddr;
+ }
+
+ boolean isConnected() {
+ return (msgType != RTM_DELNEIGH) && StructNdMsg.isNudStateConnected(nudState);
+ }
+
+ boolean isValid() {
+ return (msgType != RTM_DELNEIGH) && StructNdMsg.isNudStateValid(nudState);
+ }
+
+ @Override
+ public String toString() {
+ final StringJoiner j = new StringJoiner(",", "NeighborEvent{", "}");
+ return j.add("@" + elapsedMs)
+ .add(stringForNlMsgType(msgType, NETLINK_ROUTE))
+ .add("if=" + ifindex)
+ .add(ip.getHostAddress())
+ .add(StructNdMsg.stringForNudState(nudState))
+ .add("[" + macAddr + "]")
+ .toString();
+ }
+ }
+
+ /**
+ * A client that consumes NeighborEvent instances.
+ * Implement this to listen to neighbor events.
+ */
+ public interface NeighborEventConsumer {
+ // Every neighbor event received on the netlink socket is passed in
+ // here. Subclasses should filter for events of interest.
+ /**
+ * Consume a neighbor event
+ * @param event the event
+ */
+ void accept(NeighborEvent event);
+ }
+
+ private final NeighborEventConsumer mConsumer;
+
+ public IpNeighborMonitor(Handler h, SharedLog log, NeighborEventConsumer cb) {
+ super(h, log, TAG, NETLINK_ROUTE, OsConstants.RTMGRP_NEIGH);
+ mConsumer = (cb != null) ? cb : (event) -> { /* discard */ };
+ }
+
+ @Override
+ public void processNetlinkMessage(NetlinkMessage nlMsg, final long whenMs) {
+ if (!(nlMsg instanceof RtNetlinkNeighborMessage)) {
+ mLog.e("non-rtnetlink neighbor msg: " + nlMsg);
+ return;
+ }
+
+ final RtNetlinkNeighborMessage neighMsg = (RtNetlinkNeighborMessage) nlMsg;
+ final short msgType = neighMsg.getHeader().nlmsg_type;
+ final StructNdMsg ndMsg = neighMsg.getNdHeader();
+ if (ndMsg == null) {
+ mLog.e("RtNetlinkNeighborMessage without ND message header!");
+ return;
+ }
+
+ final int ifindex = ndMsg.ndm_ifindex;
+ final InetAddress destination = neighMsg.getDestination();
+ final short nudState =
+ (msgType == RTM_DELNEIGH)
+ ? StructNdMsg.NUD_NONE
+ : ndMsg.ndm_state;
+
+ final NeighborEvent event = new NeighborEvent(
+ whenMs, msgType, ifindex, destination, nudState,
+ getMacAddress(neighMsg.getLinkLayerAddress()));
+
+ if (VDBG) {
+ Log.d(TAG, neighMsg.toString());
+ }
+ if (DBG) {
+ Log.d(TAG, event.toString());
+ }
+
+ mConsumer.accept(event);
+ }
+
+ private static MacAddress getMacAddress(byte[] linkLayerAddress) {
+ if (linkLayerAddress != null) {
+ try {
+ return MacAddress.fromBytes(linkLayerAddress);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Failed to parse link-layer address: " + hexify(linkLayerAddress));
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/ip/NetlinkMonitor.java b/staticlibs/device/com/android/net/module/util/ip/NetlinkMonitor.java
new file mode 100644
index 0000000000..85898761ab
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/ip/NetlinkMonitor.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.ip;
+
+import static android.net.util.SocketUtils.makeNetlinkSocketAddress;
+import static android.system.OsConstants.AF_NETLINK;
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOCK_NONBLOCK;
+import static android.system.OsConstants.SOL_SOCKET;
+import static android.system.OsConstants.SO_RCVBUF;
+
+import static com.android.net.module.util.netlink.NetlinkConstants.hexify;
+
+import android.annotation.NonNull;
+import android.net.util.SocketUtils;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import com.android.net.module.util.PacketReader;
+import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.netlink.NetlinkErrorMessage;
+import com.android.net.module.util.netlink.NetlinkMessage;
+import com.android.net.module.util.netlink.NetlinkSocket;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * A simple base class to listen for netlink broadcasts.
+ *
+ * Opens a netlink socket of the given family and binds to the specified groups. Polls the socket
+ * from the event loop of the passed-in {@link Handler}, and calls the subclass-defined
+ * {@link #processNetlinkMessage} method on the handler thread for each netlink message that
+ * arrives. Currently ignores all netlink errors.
+ * @hide
+ */
+public class NetlinkMonitor extends PacketReader {
+ protected final SharedLog mLog;
+ protected final String mTag;
+ private final int mFamily;
+ private final int mBindGroups;
+ private final int mSockRcvbufSize;
+
+ private static final boolean DBG = false;
+
+ // Default socket receive buffer size. This means the specific buffer size is not set.
+ private static final int DEFAULT_SOCKET_RECV_BUFSIZE = -1;
+
+ /**
+ * Constructs a new {@code NetlinkMonitor} instance.
+ *
+ * @param h The Handler on which to poll for messages and on which to call
+ * {@link #processNetlinkMessage}.
+ * @param log A SharedLog to log to.
+ * @param tag The log tag to use for log messages.
+ * @param family the Netlink socket family to, e.g., {@code NETLINK_ROUTE}.
+ * @param bindGroups the netlink groups to bind to.
+ * @param sockRcvbufSize the specific socket receive buffer size in bytes. -1 means that don't
+ * set the specific socket receive buffer size in #createFd and use the default value in
+ * /proc/sys/net/core/rmem_default file. See SO_RCVBUF in man-pages/socket.
+ */
+ public NetlinkMonitor(@NonNull Handler h, @NonNull SharedLog log, @NonNull String tag,
+ int family, int bindGroups, int sockRcvbufSize) {
+ super(h, NetlinkSocket.DEFAULT_RECV_BUFSIZE);
+ mLog = log.forSubComponent(tag);
+ mTag = tag;
+ mFamily = family;
+ mBindGroups = bindGroups;
+ mSockRcvbufSize = sockRcvbufSize;
+ }
+
+ public NetlinkMonitor(@NonNull Handler h, @NonNull SharedLog log, @NonNull String tag,
+ int family, int bindGroups) {
+ this(h, log, tag, family, bindGroups, DEFAULT_SOCKET_RECV_BUFSIZE);
+ }
+
+ @Override
+ protected FileDescriptor createFd() {
+ FileDescriptor fd = null;
+
+ try {
+ fd = Os.socket(AF_NETLINK, SOCK_DGRAM | SOCK_NONBLOCK, mFamily);
+ if (mSockRcvbufSize != DEFAULT_SOCKET_RECV_BUFSIZE) {
+ Os.setsockoptInt(fd, SOL_SOCKET, SO_RCVBUF, mSockRcvbufSize);
+ }
+ Os.bind(fd, makeNetlinkSocketAddress(0, mBindGroups));
+ NetlinkSocket.connectToKernel(fd);
+
+ if (DBG) {
+ final SocketAddress nlAddr = Os.getsockname(fd);
+ Log.d(mTag, "bound to sockaddr_nl{" + nlAddr.toString() + "}");
+ }
+ } catch (ErrnoException | SocketException e) {
+ logError("Failed to create rtnetlink socket", e);
+ closeSocketQuietly(fd);
+ return null;
+ }
+
+ return fd;
+ }
+
+ @Override
+ protected void handlePacket(byte[] recvbuf, int length) {
+ final long whenMs = SystemClock.elapsedRealtime();
+ final ByteBuffer byteBuffer = ByteBuffer.wrap(recvbuf, 0, length);
+ byteBuffer.order(ByteOrder.nativeOrder());
+
+ while (byteBuffer.remaining() > 0) {
+ try {
+ final int position = byteBuffer.position();
+ final NetlinkMessage nlMsg = NetlinkMessage.parse(byteBuffer, mFamily);
+ if (nlMsg == null || nlMsg.getHeader() == null) {
+ byteBuffer.position(position);
+ mLog.e("unparsable netlink msg: " + hexify(byteBuffer));
+ break;
+ }
+
+ if (nlMsg instanceof NetlinkErrorMessage) {
+ mLog.e("netlink error: " + nlMsg);
+ continue;
+ }
+
+ processNetlinkMessage(nlMsg, whenMs);
+ } catch (Exception e) {
+ mLog.e("Error handling netlink message", e);
+ }
+ }
+ }
+
+ // TODO: move NetworkStackUtils to frameworks/libs/net for NetworkStackUtils#closeSocketQuietly.
+ private void closeSocketQuietly(FileDescriptor fd) {
+ try {
+ SocketUtils.closeSocket(fd);
+ } catch (IOException ignored) {
+ }
+ }
+
+ /**
+ * Processes one netlink message. Must be overridden by subclasses.
+ * @param nlMsg the message to process.
+ * @param whenMs the timestamp, as measured by {@link SystemClock#elapsedRealtime}, when the
+ * message was received.
+ */
+ protected void processNetlinkMessage(NetlinkMessage nlMsg, long whenMs) { }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/InterfaceParams.java b/staticlibs/framework/com/android/net/module/util/InterfaceParams.java
new file mode 100644
index 0000000000..30762ebb5b
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/InterfaceParams.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import android.net.MacAddress;
+import android.text.TextUtils;
+
+import java.net.NetworkInterface;
+import java.net.SocketException;
+
+
+/**
+ * Encapsulate the interface parameters common to IpClient/IpServer components.
+ *
+ * Basically all java.net.NetworkInterface methods throw Exceptions. IpClient
+ * and IpServer (sub)components need most or all of this information at some
+ * point during their lifecycles, so pass only this simplified object around
+ * which can be created once when IpClient/IpServer are told to start.
+ *
+ * @hide
+ */
+public class InterfaceParams {
+ public final String name;
+ public final int index;
+ public final boolean hasMacAddress;
+ public final MacAddress macAddr;
+ public final int defaultMtu;
+
+ // TODO: move the below to NetworkStackConstants when this class is moved to the NetworkStack.
+ private static final int ETHER_MTU = 1500;
+ private static final int IPV6_MIN_MTU = 1280;
+
+
+ /**
+ * Return InterfaceParams corresponding with an interface name
+ * @param name the interface name
+ */
+ public static InterfaceParams getByName(String name) {
+ final NetworkInterface netif = getNetworkInterfaceByName(name);
+ if (netif == null) return null;
+
+ // Not all interfaces have MAC addresses, e.g. rmnet_data0.
+ final MacAddress macAddr = getMacAddress(netif);
+
+ try {
+ return new InterfaceParams(name, netif.getIndex(), macAddr, netif.getMTU());
+ } catch (IllegalArgumentException | SocketException e) {
+ return null;
+ }
+ }
+
+ public InterfaceParams(String name, int index, MacAddress macAddr) {
+ this(name, index, macAddr, ETHER_MTU);
+ }
+
+ public InterfaceParams(String name, int index, MacAddress macAddr, int defaultMtu) {
+ if (TextUtils.isEmpty(name)) {
+ throw new IllegalArgumentException("impossible interface name");
+ }
+
+ if (index <= 0) throw new IllegalArgumentException("invalid interface index");
+
+ this.name = name;
+ this.index = index;
+ this.hasMacAddress = (macAddr != null);
+ this.macAddr = hasMacAddress ? macAddr : MacAddress.fromBytes(new byte[] {
+ 0x02, 0x00, 0x00, 0x00, 0x00, 0x00 });
+ this.defaultMtu = (defaultMtu > IPV6_MIN_MTU) ? defaultMtu : IPV6_MIN_MTU;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s/%d/%s/%d", name, index, macAddr, defaultMtu);
+ }
+
+ private static NetworkInterface getNetworkInterfaceByName(String name) {
+ try {
+ return NetworkInterface.getByName(name);
+ } catch (NullPointerException | SocketException e) {
+ return null;
+ }
+ }
+
+ private static MacAddress getMacAddress(NetworkInterface netif) {
+ try {
+ return MacAddress.fromBytes(netif.getHardwareAddress());
+ } catch (IllegalArgumentException | NullPointerException | SocketException e) {
+ return null;
+ }
+ }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/RouteUtils.java b/staticlibs/framework/com/android/net/module/util/RouteUtils.java
new file mode 100644
index 0000000000..c2416802a7
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/RouteUtils.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+/** @hide */
+// RouteUtils is now empty, because some new methods will be added to it soon and it is less
+// expensive to keep it empty than to remove it now and add it again later.
+public class RouteUtils {
+}
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index 23835fe918..a9f9d70b27 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -13,10 +13,12 @@ android_library {
defaults: ["framework-connectivity-test-defaults"],
static_libs: [
"net-utils-framework-common",
+ "net-utils-device-common-ip",
"androidx.test.rules",
"mockito-target-extended-minus-junit4",
"net-utils-device-common",
"net-tests-utils",
+ "netd-client",
],
libs: [
"android.test.runner",
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/SharedLogTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/SharedLogTest.java
new file mode 100644
index 0000000000..446e8819fd
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/SharedLogTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintWriter;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class SharedLogTest {
+ private static final String TIMESTAMP_PATTERN = "\\d{2}:\\d{2}:\\d{2}";
+ private static final String TIMESTAMP = "HH:MM:SS";
+
+ @Test
+ public void testBasicOperation() {
+ final SharedLog logTop = new SharedLog("top");
+ logTop.mark("first post!");
+
+ final SharedLog logLevel2a = logTop.forSubComponent("twoA");
+ final SharedLog logLevel2b = logTop.forSubComponent("twoB");
+ logLevel2b.e("2b or not 2b");
+ logLevel2b.e("No exception", null);
+ logLevel2b.e("Wait, here's one", new Exception("Test"));
+ logLevel2a.w("second post?");
+
+ final SharedLog logLevel3 = logLevel2a.forSubComponent("three");
+ logTop.log("still logging");
+ logLevel3.log("3 >> 2");
+ logLevel2a.mark("ok: last post");
+
+ final String[] expected = {
+ " - MARK first post!",
+ " - [twoB] ERROR 2b or not 2b",
+ " - [twoB] ERROR No exception",
+ // No stacktrace in shared log, only in logcat
+ " - [twoB] ERROR Wait, here's one: Test",
+ " - [twoA] WARN second post?",
+ " - still logging",
+ " - [twoA.three] 3 >> 2",
+ " - [twoA] MARK ok: last post",
+ };
+ // Verify the logs are all there and in the correct order.
+ verifyLogLines(expected, logTop);
+
+ // In fact, because they all share the same underlying LocalLog,
+ // every subcomponent SharedLog's dump() is identical.
+ verifyLogLines(expected, logLevel2a);
+ verifyLogLines(expected, logLevel2b);
+ verifyLogLines(expected, logLevel3);
+ }
+
+ private static void verifyLogLines(String[] expected, SharedLog log) {
+ final ByteArrayOutputStream ostream = new ByteArrayOutputStream();
+ final PrintWriter pw = new PrintWriter(ostream, true);
+ log.dump(null, pw, null);
+
+ final String dumpOutput = ostream.toString();
+ assertTrue(dumpOutput != null);
+ assertTrue(!"".equals(dumpOutput));
+
+ final String[] lines = dumpOutput.split("\n");
+ assertEquals(expected.length, lines.length);
+
+ for (int i = 0; i < expected.length; i++) {
+ String got = lines[i];
+ String want = expected[i];
+ assertTrue(String.format("'%s' did not contain '%s'", got, want), got.endsWith(want));
+ assertTrue(String.format("'%s' did not contain a %s timestamp", got, TIMESTAMP),
+ got.replaceFirst(TIMESTAMP_PATTERN, TIMESTAMP).contains(TIMESTAMP));
+ }
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/ip/ConntrackMonitorTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/ip/ConntrackMonitorTest.java
new file mode 100644
index 0000000000..99d8f0739d
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/ip/ConntrackMonitorTest.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.ip;
+
+import static android.system.OsConstants.AF_UNIX;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.SOCK_DGRAM;
+
+import static com.android.net.module.util.netlink.ConntrackMessage.Tuple;
+import static com.android.net.module.util.netlink.ConntrackMessage.TupleIpv4;
+import static com.android.net.module.util.netlink.ConntrackMessage.TupleProto;
+import static com.android.net.module.util.netlink.NetlinkConstants.IPCTNL_MSG_CT_DELETE;
+import static com.android.net.module.util.netlink.NetlinkConstants.IPCTNL_MSG_CT_NEW;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.net.InetAddresses;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import androidx.annotation.NonNull;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.ip.ConntrackMonitor.ConntrackEvent;
+import com.android.net.module.util.netlink.NetlinkConstants;
+import com.android.net.module.util.netlink.NetlinkSocket;
+
+import libcore.util.HexEncoding;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.FileDescriptor;
+import java.io.InterruptedIOException;
+import java.net.Inet4Address;
+
+/**
+ * Tests for ConntrackMonitor.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ConntrackMonitorTest {
+ private static final long TIMEOUT_MS = 10_000L;
+
+ @Mock private SharedLog mLog;
+ @Mock private ConntrackMonitor.ConntrackEventConsumer mConsumer;
+
+ private final HandlerThread mHandlerThread = new HandlerThread(
+ ConntrackMonitorTest.class.getSimpleName());
+
+ // Late init since the handler thread has been started.
+ private Handler mHandler;
+ private TestConntrackMonitor mConntrackMonitor;
+
+ // A version of [ConntrackMonitor] that reads packets from the socket pair, and instead
+ // allows the test to write test packets to the socket pair via [sendMessage].
+ private class TestConntrackMonitor extends ConntrackMonitor {
+ TestConntrackMonitor(@NonNull Handler h, @NonNull SharedLog log,
+ @NonNull ConntrackEventConsumer cb) {
+ super(h, log, cb);
+
+ mReadFd = new FileDescriptor();
+ mWriteFd = new FileDescriptor();
+ try {
+ Os.socketpair(AF_UNIX, SOCK_DGRAM, 0, mWriteFd, mReadFd);
+ } catch (ErrnoException e) {
+ fail("Could not create socket pair: " + e);
+ }
+ }
+
+ @Override
+ protected FileDescriptor createFd() {
+ return mReadFd;
+ }
+
+ private void sendMessage(byte[] msg) {
+ mHandler.post(() -> {
+ try {
+ NetlinkSocket.sendMessage(mWriteFd, msg, 0 /* offset */, msg.length,
+ TIMEOUT_MS);
+ } catch (ErrnoException | InterruptedIOException e) {
+ fail("Unable to send netfilter message: " + e);
+ }
+ });
+ }
+
+ private final FileDescriptor mReadFd;
+ private final FileDescriptor mWriteFd;
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
+
+ // ConntrackMonitor needs to be started from the handler thread.
+ final ConditionVariable initDone = new ConditionVariable();
+ mHandler.post(() -> {
+ TestConntrackMonitor m = new TestConntrackMonitor(mHandler, mLog, mConsumer);
+ m.start();
+ mConntrackMonitor = m;
+
+ initDone.open();
+ });
+ if (!initDone.block(TIMEOUT_MS)) {
+ fail("... init monitor timed-out after " + TIMEOUT_MS + "ms");
+ }
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mHandlerThread.quitSafely();
+ }
+
+ public static final String CT_V4NEW_TCP_HEX =
+ // CHECKSTYLE:OFF IndentationCheck
+ // struct nlmsghdr
+ "8C000000" + // length = 140
+ "0001" + // type = NFNL_SUBSYS_CTNETLINK (1) << 8 | IPCTNL_MSG_CT_NEW (0)
+ "0006" + // flags = NLM_F_CREATE (1 << 10) | NLM_F_EXCL (1 << 9)
+ "00000000" + // seqno = 0
+ "00000000" + // pid = 0
+ // struct nfgenmsg
+ "02" + // nfgen_family = AF_INET
+ "00" + // version = NFNETLINK_V0
+ "1234" + // res_id = 0x1234 (big endian)
+ // struct nlattr
+ "3400" + // nla_len = 52
+ "0180" + // nla_type = nested CTA_TUPLE_ORIG
+ // struct nlattr
+ "1400" + // nla_len = 20
+ "0180" + // nla_type = nested CTA_TUPLE_IP
+ "0800 0100 C0A8500C" + // nla_type=CTA_IP_V4_SRC, ip=192.168.80.12
+ "0800 0200 8C700874" + // nla_type=CTA_IP_V4_DST, ip=140.112.8.116
+ // struct nlattr
+ "1C00" + // nla_len = 28
+ "0280" + // nla_type = nested CTA_TUPLE_PROTO
+ "0500 0100 06 000000" + // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
+ "0600 0200 F3F1 0000" + // nla_type=CTA_PROTO_SRC_PORT, port=62449 (big endian)
+ "0600 0300 01BB 0000" + // nla_type=CTA_PROTO_DST_PORT, port=443 (big endian)
+ // struct nlattr
+ "3400" + // nla_len = 52
+ "0280" + // nla_type = nested CTA_TUPLE_REPLY
+ // struct nlattr
+ "1400" + // nla_len = 20
+ "0180" + // nla_type = nested CTA_TUPLE_IP
+ "0800 0100 8C700874" + // nla_type=CTA_IP_V4_SRC, ip=140.112.8.116
+ "0800 0200 6451B301" + // nla_type=CTA_IP_V4_DST, ip=100.81.179.1
+ // struct nlattr
+ "1C00" + // nla_len = 28
+ "0280" + // nla_type = nested CTA_TUPLE_PROTO
+ "0500 0100 06 000000" + // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
+ "0600 0200 01BB 0000" + // nla_type=CTA_PROTO_SRC_PORT, port=443 (big endian)
+ "0600 0300 F3F1 0000" + // nla_type=CTA_PROTO_DST_PORT, port=62449 (big endian)
+ // struct nlattr
+ "0800" + // nla_len = 8
+ "0300" + // nla_type = CTA_STATUS
+ "0000019e" + // nla_value = 0b110011110 (big endian)
+ // IPS_SEEN_REPLY (1 << 1) | IPS_ASSURED (1 << 2) |
+ // IPS_CONFIRMED (1 << 3) | IPS_SRC_NAT (1 << 4) |
+ // IPS_SRC_NAT_DONE (1 << 7) | IPS_DST_NAT_DONE (1 << 8)
+ // struct nlattr
+ "0800" + // nla_len = 8
+ "0700" + // nla_type = CTA_TIMEOUT
+ "00000078"; // nla_value = 120 (big endian)
+ // CHECKSTYLE:ON IndentationCheck
+ public static final byte[] CT_V4NEW_TCP_BYTES =
+ HexEncoding.decode(CT_V4NEW_TCP_HEX.replaceAll(" ", "").toCharArray(), false);
+
+ @NonNull
+ private ConntrackEvent makeTestConntrackEvent(short msgType, int status, int timeoutSec) {
+ final Inet4Address privateIp =
+ (Inet4Address) InetAddresses.parseNumericAddress("192.168.80.12");
+ final Inet4Address remoteIp =
+ (Inet4Address) InetAddresses.parseNumericAddress("140.112.8.116");
+ final Inet4Address publicIp =
+ (Inet4Address) InetAddresses.parseNumericAddress("100.81.179.1");
+
+ return new ConntrackEvent(
+ (short) (NetlinkConstants.NFNL_SUBSYS_CTNETLINK << 8 | msgType),
+ new Tuple(new TupleIpv4(privateIp, remoteIp),
+ new TupleProto((byte) IPPROTO_TCP, (short) 62449, (short) 443)),
+ new Tuple(new TupleIpv4(remoteIp, publicIp),
+ new TupleProto((byte) IPPROTO_TCP, (short) 443, (short) 62449)),
+ status,
+ timeoutSec);
+ }
+
+ @Test
+ public void testConntrackEventNew() throws Exception {
+ final ConntrackEvent expectedEvent = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW,
+ 0x19e /* status */, 120 /* timeoutSec */);
+ mConntrackMonitor.sendMessage(CT_V4NEW_TCP_BYTES);
+ verify(mConsumer, timeout(TIMEOUT_MS)).accept(eq(expectedEvent));
+ }
+
+ @Test
+ public void testConntrackEventEquals() {
+ final ConntrackEvent event1 = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, 1234 /* status */,
+ 5678 /* timeoutSec*/);
+ final ConntrackEvent event2 = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, 1234 /* status */,
+ 5678 /* timeoutSec*/);
+ assertEquals(event1, event2);
+ }
+
+ @Test
+ public void testConntrackEventNotEquals() {
+ final ConntrackEvent e = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, 1234 /* status */,
+ 5678 /* timeoutSec*/);
+
+ final ConntrackEvent typeNotEqual = new ConntrackEvent((short) (e.msgType + 1) /* diff */,
+ e.tupleOrig, e.tupleReply, e.status, e.timeoutSec);
+ assertNotEquals(e, typeNotEqual);
+
+ final ConntrackEvent tupleOrigNotEqual = new ConntrackEvent(e.msgType,
+ null /* diff */, e.tupleReply, e.status, e.timeoutSec);
+ assertNotEquals(e, tupleOrigNotEqual);
+
+ final ConntrackEvent tupleReplyNotEqual = new ConntrackEvent(e.msgType,
+ e.tupleOrig, null /* diff */, e.status, e.timeoutSec);
+ assertNotEquals(e, tupleReplyNotEqual);
+
+ final ConntrackEvent statusNotEqual = new ConntrackEvent(e.msgType,
+ e.tupleOrig, e.tupleReply, e.status + 1 /* diff */, e.timeoutSec);
+ assertNotEquals(e, statusNotEqual);
+
+ final ConntrackEvent timeoutSecNotEqual = new ConntrackEvent(e.msgType,
+ e.tupleOrig, e.tupleReply, e.status, e.timeoutSec + 1 /* diff */);
+ assertNotEquals(e, timeoutSecNotEqual);
+ }
+
+ @Test
+ public void testToString() {
+ final ConntrackEvent event = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW,
+ 0x198 /* status */, 120 /* timeoutSec */);
+ final String expected = ""
+ + "ConntrackEvent{"
+ + "msg_type{IPCTNL_MSG_CT_NEW}, "
+ + "tuple_orig{Tuple{IPPROTO_TCP: 192.168.80.12:62449 -> 140.112.8.116:443}}, "
+ + "tuple_reply{Tuple{IPPROTO_TCP: 140.112.8.116:443 -> 100.81.179.1:62449}}, "
+ + "status{408(IPS_CONFIRMED|IPS_SRC_NAT|IPS_SRC_NAT_DONE|IPS_DST_NAT_DONE)}, "
+ + "timeout_sec{120}}";
+ assertEquals(expected, event.toString());
+ }
+
+ public static final String CT_V4DELETE_TCP_HEX =
+ // CHECKSTYLE:OFF IndentationCheck
+ // struct nlmsghdr
+ "84000000" + // length = 132
+ "0201" + // type = NFNL_SUBSYS_CTNETLINK (1) << 8 | IPCTNL_MSG_CT_DELETE (2)
+ "0000" + // flags = 0
+ "00000000" + // seqno = 0
+ "00000000" + // pid = 0
+ // struct nfgenmsg
+ "02" + // nfgen_family = AF_INET
+ "00" + // version = NFNETLINK_V0
+ "1234" + // res_id = 0x1234 (big endian)
+ // struct nlattr
+ "3400" + // nla_len = 52
+ "0180" + // nla_type = nested CTA_TUPLE_ORIG
+ // struct nlattr
+ "1400" + // nla_len = 20
+ "0180" + // nla_type = nested CTA_TUPLE_IP
+ "0800 0100 C0A8500C" + // nla_type=CTA_IP_V4_SRC, ip=192.168.80.12
+ "0800 0200 8C700874" + // nla_type=CTA_IP_V4_DST, ip=140.112.8.116
+ // struct nlattr
+ "1C00" + // nla_len = 28
+ "0280" + // nla_type = nested CTA_TUPLE_PROTO
+ "0500 0100 06 000000" + // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
+ "0600 0200 F3F1 0000" + // nla_type=CTA_PROTO_SRC_PORT, port=62449 (big endian)
+ "0600 0300 01BB 0000" + // nla_type=CTA_PROTO_DST_PORT, port=433 (big endian)
+ // struct nlattr
+ "3400" + // nla_len = 52
+ "0280" + // nla_type = nested CTA_TUPLE_REPLY
+ // struct nlattr
+ "1400" + // nla_len = 20
+ "0180" + // nla_type = nested CTA_TUPLE_IP
+ "0800 0100 8C700874" + // nla_type=CTA_IP_V4_SRC, ip=140.112.8.116
+ "0800 0200 6451B301" + // nla_type=CTA_IP_V4_DST, ip=100.81.179.1
+ // struct nlattr
+ "1C00" + // nla_len = 28
+ "0280" + // nla_type = nested CTA_TUPLE_PROTO
+ "0500 0100 06 000000" + // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
+ "0600 0200 01BB 0000" + // nla_type=CTA_PROTO_SRC_PORT, port=433 (big endian)
+ "0600 0300 F3F1 0000" + // nla_type=CTA_PROTO_DST_PORT, port=62449 (big endian)
+ // struct nlattr
+ "0800" + // nla_len = 8
+ "0300" + // nla_type = CTA_STATUS
+ "0000039E"; // nla_value = 0b1110011110 (big endian)
+ // IPS_SEEN_REPLY (1 << 1) | IPS_ASSURED (1 << 2) |
+ // IPS_CONFIRMED (1 << 3) | IPS_SRC_NAT (1 << 4) |
+ // IPS_SRC_NAT_DONE (1 << 7) | IPS_DST_NAT_DONE (1 << 8) |
+ // IPS_DYING (1 << 9)
+ // CHECKSTYLE:ON IndentationCheck
+ public static final byte[] CT_V4DELETE_TCP_BYTES =
+ HexEncoding.decode(CT_V4DELETE_TCP_HEX.replaceAll(" ", "").toCharArray(), false);
+
+ @Test
+ public void testConntrackEventDelete() throws Exception {
+ final ConntrackEvent expectedEvent =
+ makeTestConntrackEvent(IPCTNL_MSG_CT_DELETE, 0x39e /* status */,
+ 0 /* timeoutSec (absent) */);
+ mConntrackMonitor.sendMessage(CT_V4DELETE_TCP_BYTES);
+ verify(mConsumer, timeout(TIMEOUT_MS)).accept(eq(expectedEvent));
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/ip/InterfaceControllerTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/ip/InterfaceControllerTest.java
new file mode 100644
index 0000000000..dea667d815
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/ip/InterfaceControllerTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.ip;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.net.INetd;
+import android.net.InetAddresses;
+import android.net.InterfaceConfigurationParcel;
+import android.net.LinkAddress;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.SharedLog;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class InterfaceControllerTest {
+ private static final String TEST_IFACE = "testif";
+ private static final String TEST_IPV4_ADDR = "192.168.123.28";
+ private static final int TEST_PREFIXLENGTH = 31;
+
+ @Mock private INetd mNetd;
+ @Mock private SharedLog mLog;
+ @Captor private ArgumentCaptor