Merge "Move static utils to the static library"
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index a7a027c..4fc93ce 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -38,6 +38,7 @@
       "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 @@
 
 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: [
@@ -125,6 +126,36 @@
 }
 
 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: [
         ":net-utils-framework-common-srcs",
@@ -205,7 +236,11 @@
 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 0000000..37c6f6d
--- /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.
+     *
+     * <p>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.
+     *
+     * <p>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.
+     *
+     * <p>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.
+     *
+     * <p>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.
+     *
+     * <p>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.
+     *
+     * <p>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.
+     *
+     * <p>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.
+     *
+     * <p>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.
+     *
+     * <p>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<String> mLog;
+        private final int mMaxLines;
+
+        LocalLog(int maxLines) {
+            mMaxLines = Math.max(0, maxLines);
+            mLog = new ArrayDeque<>(mMaxLines);
+        }
+
+        synchronized void append(String logLine) {
+            if (mMaxLines <= 0) return;
+            while (mLog.size() >= mMaxLines) {
+                mLog.remove();
+            }
+            mLog.add(LocalDateTime.now() + " - " + logLine);
+        }
+
+        /**
+         * Dumps the content of local log to print writer with each log entry
+         *
+         * @param pw printer writer to write into
+         */
+        synchronized void dump(PrintWriter pw) {
+            for (final String s : mLog) {
+                pw.println(s);
+            }
+        }
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/ip/ConntrackMonitor.java b/staticlibs/device/com/android/net/module/util/ip/ConntrackMonitor.java
new file mode 100644
index 0000000..420a544
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/ip/ConntrackMonitor.java
@@ -0,0 +1,203 @@
+/*
+ * 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 com.android.net.module.util.netlink.ConntrackMessage.DYING_MASK;
+import static com.android.net.module.util.netlink.ConntrackMessage.ESTABLISHED_MASK;
+
+import android.annotation.NonNull;
+import android.os.Handler;
+import android.system.OsConstants;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.netlink.ConntrackMessage;
+import com.android.net.module.util.netlink.NetlinkConstants;
+import com.android.net.module.util.netlink.NetlinkMessage;
+
+import java.util.Objects;
+
+
+/**
+ * ConntrackMonitor.
+ *
+ * Monitors the netfilter conntrack notifications and presents to callers
+ * ConntrackEvents describing each event.
+ *
+ * @hide
+ */
+public class ConntrackMonitor extends NetlinkMonitor {
+    private static final String TAG = ConntrackMonitor.class.getSimpleName();
+    private static final boolean DBG = false;
+    private static final boolean VDBG = false;
+
+    // Reference kernel/uapi/linux/netfilter/nfnetlink_compat.h
+    public static final int NF_NETLINK_CONNTRACK_NEW = 1;
+    public static final int NF_NETLINK_CONNTRACK_UPDATE = 2;
+    public static final int NF_NETLINK_CONNTRACK_DESTROY = 4;
+
+    // The socket receive buffer size in bytes. If too many conntrack messages are sent too
+    // quickly, the conntrack messages can overflow the socket receive buffer. This can happen
+    // if too many connections are disconnected by losing network and so on. Use a large-enough
+    // buffer to avoid the error ENOBUFS while listening to the conntrack messages.
+    private static final int SOCKET_RECV_BUFSIZE = 6 * 1024 * 1024;
+
+    /**
+     * A class for describing parsed netfilter conntrack events.
+     */
+    public static class ConntrackEvent {
+        /**
+         * Conntrack event type.
+         */
+        public final short msgType;
+        /**
+         * Original direction conntrack tuple.
+         */
+        public final ConntrackMessage.Tuple tupleOrig;
+        /**
+         * Reply direction conntrack tuple.
+         */
+        public final ConntrackMessage.Tuple tupleReply;
+        /**
+         * Connection status. A bitmask of ip_conntrack_status enum flags.
+         */
+        public final int status;
+        /**
+         * Conntrack timeout.
+         */
+        public final int timeoutSec;
+
+        public ConntrackEvent(ConntrackMessage msg) {
+            this.msgType = msg.getHeader().nlmsg_type;
+            this.tupleOrig = msg.tupleOrig;
+            this.tupleReply = msg.tupleReply;
+            this.status = msg.status;
+            this.timeoutSec = msg.timeoutSec;
+        }
+
+        @VisibleForTesting
+        public ConntrackEvent(short msgType, ConntrackMessage.Tuple tupleOrig,
+                ConntrackMessage.Tuple tupleReply, int status, int timeoutSec) {
+            this.msgType = msgType;
+            this.tupleOrig = tupleOrig;
+            this.tupleReply = tupleReply;
+            this.status = status;
+            this.timeoutSec = timeoutSec;
+        }
+
+        @Override
+        @VisibleForTesting
+        public boolean equals(Object o) {
+            if (!(o instanceof ConntrackEvent)) return false;
+            ConntrackEvent that = (ConntrackEvent) o;
+            return this.msgType == that.msgType
+                    && Objects.equals(this.tupleOrig, that.tupleOrig)
+                    && Objects.equals(this.tupleReply, that.tupleReply)
+                    && this.status == that.status
+                    && this.timeoutSec == that.timeoutSec;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(msgType, tupleOrig, tupleReply, status, timeoutSec);
+        }
+
+        @Override
+        public String toString() {
+            return "ConntrackEvent{"
+                    + "msg_type{"
+                    + NetlinkConstants.stringForNlMsgType(msgType, OsConstants.NETLINK_NETFILTER)
+                    + "}, "
+                    + "tuple_orig{" + tupleOrig + "}, "
+                    + "tuple_reply{" + tupleReply + "}, "
+                    + "status{"
+                    + status + "(" + ConntrackMessage.stringForIpConntrackStatus(status) + ")"
+                    + "}, "
+                    + "timeout_sec{" + Integer.toUnsignedLong(timeoutSec) + "}"
+                    + "}";
+        }
+
+        /**
+         * Check the established NAT session conntrack message.
+         *
+         * @param msg the conntrack message to check.
+         * @return true if an established NAT message, false if not.
+         */
+        public static boolean isEstablishedNatSession(@NonNull ConntrackMessage msg) {
+            if (msg.getMessageType() != NetlinkConstants.IPCTNL_MSG_CT_NEW) return false;
+            if (msg.tupleOrig == null) return false;
+            if (msg.tupleReply == null) return false;
+            if (msg.timeoutSec == 0) return false;
+            if ((msg.status & ESTABLISHED_MASK) != ESTABLISHED_MASK) return false;
+
+            return true;
+        }
+
+        /**
+         * Check the dying NAT session conntrack message.
+         * Note that IPCTNL_MSG_CT_DELETE event has no CTA_TIMEOUT attribute.
+         *
+         * @param msg the conntrack message to check.
+         * @return true if a dying NAT message, false if not.
+         */
+        public static boolean isDyingNatSession(@NonNull ConntrackMessage msg) {
+            if (msg.getMessageType() != NetlinkConstants.IPCTNL_MSG_CT_DELETE) return false;
+            if (msg.tupleOrig == null) return false;
+            if (msg.tupleReply == null) return false;
+            if (msg.timeoutSec != 0) return false;
+            if ((msg.status & DYING_MASK) != DYING_MASK) return false;
+
+            return true;
+        }
+    }
+
+    /**
+     * A callback to caller for conntrack event.
+     */
+    public interface ConntrackEventConsumer {
+        /**
+         * Every conntrack event received on the netlink socket is passed in
+         * here.
+         */
+        void accept(@NonNull ConntrackEvent event);
+    }
+
+    private final ConntrackEventConsumer mConsumer;
+
+    public ConntrackMonitor(@NonNull Handler h, @NonNull SharedLog log,
+            @NonNull ConntrackEventConsumer cb) {
+        super(h, log, TAG, OsConstants.NETLINK_NETFILTER, NF_NETLINK_CONNTRACK_NEW
+                | NF_NETLINK_CONNTRACK_UPDATE | NF_NETLINK_CONNTRACK_DESTROY, SOCKET_RECV_BUFSIZE);
+        mConsumer = cb;
+    }
+
+    @Override
+    public void processNetlinkMessage(NetlinkMessage nlMsg, final long whenMs) {
+        if (!(nlMsg instanceof ConntrackMessage)) {
+            mLog.e("non-conntrack msg: " + nlMsg);
+            return;
+        }
+
+        final ConntrackMessage conntrackMsg = (ConntrackMessage) nlMsg;
+        if (!(ConntrackEvent.isEstablishedNatSession(conntrackMsg)
+                || ConntrackEvent.isDyingNatSession(conntrackMsg))) {
+            return;
+        }
+
+        mConsumer.accept(new ConntrackEvent(conntrackMsg));
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/ip/InterfaceController.java b/staticlibs/device/com/android/net/module/util/ip/InterfaceController.java
new file mode 100644
index 0000000..7277fec
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/ip/InterfaceController.java
@@ -0,0 +1,211 @@
+/*
+ * 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 android.net.INetd.IF_STATE_DOWN;
+import static android.net.INetd.IF_STATE_UP;
+
+import android.net.INetd;
+import android.net.InterfaceConfigurationParcel;
+import android.net.LinkAddress;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.system.OsConstants;
+
+import com.android.net.module.util.SharedLog;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+
+/**
+ * Encapsulates the multiple IP configuration operations performed on an interface.
+ *
+ * TODO: refactor/eliminate the redundant ways to set and clear addresses.
+ *
+ * @hide
+ */
+public class InterfaceController {
+    private static final boolean DBG = false;
+
+    private final String mIfName;
+    private final INetd mNetd;
+    private final SharedLog mLog;
+
+    public InterfaceController(String ifname, INetd netd, SharedLog log) {
+        mIfName = ifname;
+        mNetd = netd;
+        mLog = log;
+    }
+
+    /**
+     * Set the IPv4 address and also optionally bring the interface up or down.
+     */
+    public boolean setInterfaceConfiguration(final LinkAddress ipv4Addr,
+            final Boolean setIfaceUp) {
+        if (!(ipv4Addr.getAddress() instanceof Inet4Address)) {
+            throw new IllegalArgumentException("Invalid or mismatched Inet4Address");
+        }
+        // Note: currently netd only support INetd#IF_STATE_UP and #IF_STATE_DOWN.
+        // Other flags would be ignored.
+
+        final InterfaceConfigurationParcel ifConfig = new InterfaceConfigurationParcel();
+        ifConfig.ifName = mIfName;
+        ifConfig.ipv4Addr = ipv4Addr.getAddress().getHostAddress();
+        ifConfig.prefixLength = ipv4Addr.getPrefixLength();
+        // Netd ignores hwaddr in interfaceSetCfg.
+        ifConfig.hwAddr = "";
+        if (setIfaceUp == null) {
+            // Empty array means no change.
+            ifConfig.flags = new String[0];
+        } else {
+            // Netd ignores any flag that's not IF_STATE_UP or IF_STATE_DOWN in interfaceSetCfg.
+            ifConfig.flags = setIfaceUp.booleanValue()
+                    ? new String[] {IF_STATE_UP} : new String[] {IF_STATE_DOWN};
+        }
+        try {
+            mNetd.interfaceSetCfg(ifConfig);
+        } catch (RemoteException | ServiceSpecificException e) {
+            logError("Setting IPv4 address to %s/%d failed: %s",
+                    ifConfig.ipv4Addr, ifConfig.prefixLength, e);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Set the IPv4 address of the interface.
+     */
+    public boolean setIPv4Address(final LinkAddress address) {
+        return setInterfaceConfiguration(address, null);
+    }
+
+    /**
+     * Clear the IPv4Address of the interface.
+     */
+    public boolean clearIPv4Address() {
+        return setIPv4Address(new LinkAddress("0.0.0.0/0"));
+    }
+
+    private boolean setEnableIPv6(boolean enabled) {
+        try {
+            mNetd.interfaceSetEnableIPv6(mIfName, enabled);
+        } catch (RemoteException | ServiceSpecificException e) {
+            logError("%s IPv6 failed: %s", (enabled ? "enabling" : "disabling"), e);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Enable IPv6 on the interface.
+     */
+    public boolean enableIPv6() {
+        return setEnableIPv6(true);
+    }
+
+    /**
+     * Disable IPv6 on the interface.
+     */
+    public boolean disableIPv6() {
+        return setEnableIPv6(false);
+    }
+
+    /**
+     * Enable or disable IPv6 privacy extensions on the interface.
+     * @param enabled Whether the extensions should be enabled.
+     */
+    public boolean setIPv6PrivacyExtensions(boolean enabled) {
+        try {
+            mNetd.interfaceSetIPv6PrivacyExtensions(mIfName, enabled);
+        } catch (RemoteException | ServiceSpecificException e) {
+            logError("error %s IPv6 privacy extensions: %s",
+                    (enabled ? "enabling" : "disabling"), e);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Set IPv6 address generation mode on the interface.
+     *
+     * <p>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 0000000..88f8c9d
--- /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 0000000..8589876
--- /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 0000000..30762eb
--- /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 0000000..c241680
--- /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 23835fe..a9f9d70 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -13,10 +13,12 @@
     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 0000000..446e881
--- /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 0000000..99d8f07
--- /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 0000000..dea667d
--- /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<InterfaceConfigurationParcel> mConfigCaptor;
+
+    private InterfaceController mController;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mController = new InterfaceController(TEST_IFACE, mNetd, mLog);
+
+        doNothing().when(mNetd).interfaceSetCfg(mConfigCaptor.capture());
+    }
+
+    @Test
+    public void testSetIPv4Address() throws Exception {
+        mController.setIPv4Address(
+                new LinkAddress(InetAddresses.parseNumericAddress(TEST_IPV4_ADDR),
+                        TEST_PREFIXLENGTH));
+        verify(mNetd, times(1)).interfaceSetCfg(any());
+        final InterfaceConfigurationParcel parcel = mConfigCaptor.getValue();
+        assertEquals(TEST_IFACE, parcel.ifName);
+        assertEquals(TEST_IPV4_ADDR, parcel.ipv4Addr);
+        assertEquals(TEST_PREFIXLENGTH, parcel.prefixLength);
+        assertEquals("", parcel.hwAddr);
+        assertArrayEquals(new String[0], parcel.flags);
+    }
+
+    @Test
+    public void testClearIPv4Address() throws Exception {
+        mController.clearIPv4Address();
+        verify(mNetd, times(1)).interfaceSetCfg(any());
+        final InterfaceConfigurationParcel parcel = mConfigCaptor.getValue();
+        assertEquals(TEST_IFACE, parcel.ifName);
+        assertEquals("0.0.0.0", parcel.ipv4Addr);
+        assertEquals(0, parcel.prefixLength);
+        assertEquals("", parcel.hwAddr);
+        assertArrayEquals(new String[0], parcel.flags);
+    }
+}