Merge "Use ACCESS_WIFI_STATE when clearing wifi blacklist" into mainline-prod
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 83a4ebc..0957526 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -66,6 +66,9 @@
         "liblog",
         "libnativehelper_compat_libc++",
     ],
+    static_libs: [
+        "libnetjniutils",
+    ],
 
     // We cannot use plain "libc++" here to link libc++ dynamically because it results in:
     //   java.lang.UnsatisfiedLinkError: dlopen failed: library "libc++_shared.so" not found
diff --git a/Tethering/jni/android_net_util_TetheringUtils.cpp b/Tethering/jni/android_net_util_TetheringUtils.cpp
index 5493440..7bfb6da 100644
--- a/Tethering/jni/android_net_util_TetheringUtils.cpp
+++ b/Tethering/jni/android_net_util_TetheringUtils.cpp
@@ -17,23 +17,69 @@
 #include <errno.h>
 #include <error.h>
 #include <jni.h>
+#include <linux/filter.h>
 #include <nativehelper/JNIHelp.h>
 #include <nativehelper/ScopedUtfChars.h>
+#include <netjniutils/netjniutils.h>
 #include <net/if.h>
+#include <netinet/ether.h>
+#include <netinet/ip6.h>
 #include <netinet/icmp6.h>
 #include <sys/socket.h>
+#include <stdio.h>
 
 #define LOG_TAG "TetheringUtils"
 #include <android/log.h>
 
 namespace android {
 
+static const uint32_t kIPv6NextHeaderOffset = offsetof(ip6_hdr, ip6_nxt);
+static const uint32_t kIPv6PayloadStart = sizeof(ip6_hdr);
+static const uint32_t kICMPv6TypeOffset = kIPv6PayloadStart + offsetof(icmp6_hdr, icmp6_type);
+
+static void android_net_util_setupIcmpFilter(JNIEnv *env, jobject javaFd, uint32_t type) {
+    sock_filter filter_code[] = {
+        // Check header is ICMPv6.
+        BPF_STMT(BPF_LD  | BPF_B   | BPF_ABS,  kIPv6NextHeaderOffset),
+        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,    IPPROTO_ICMPV6, 0, 3),
+
+        // Check ICMPv6 type.
+        BPF_STMT(BPF_LD  | BPF_B   | BPF_ABS,  kICMPv6TypeOffset),
+        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,    type, 0, 1),
+
+        // Accept or reject.
+        BPF_STMT(BPF_RET | BPF_K,              0xffff),
+        BPF_STMT(BPF_RET | BPF_K,              0)
+    };
+
+    const sock_fprog filter = {
+        sizeof(filter_code) / sizeof(filter_code[0]),
+        filter_code,
+    };
+
+    int fd = netjniutils::GetNativeFileDescriptor(env, javaFd);
+    if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) != 0) {
+        jniThrowExceptionFmt(env, "java/net/SocketException",
+                "setsockopt(SO_ATTACH_FILTER): %s", strerror(errno));
+    }
+}
+
+static void android_net_util_setupNaSocket(JNIEnv *env, jobject clazz, jobject javaFd)
+{
+    android_net_util_setupIcmpFilter(env, javaFd, ND_NEIGHBOR_ADVERT);
+}
+
+static void android_net_util_setupNsSocket(JNIEnv *env, jobject clazz, jobject javaFd)
+{
+    android_net_util_setupIcmpFilter(env, javaFd, ND_NEIGHBOR_SOLICIT);
+}
+
 static void android_net_util_setupRaSocket(JNIEnv *env, jobject clazz, jobject javaFd,
         jint ifIndex)
 {
     static const int kLinkLocalHopLimit = 255;
 
-    int fd = jniGetFDFromFileDescriptor(env, javaFd);
+    int fd = netjniutils::GetNativeFileDescriptor(env, javaFd);
 
     // Set an ICMPv6 filter that only passes Router Solicitations.
     struct icmp6_filter rs_only;
@@ -124,7 +170,12 @@
  */
 static const JNINativeMethod gMethods[] = {
     /* name, signature, funcPtr */
-    { "setupRaSocket", "(Ljava/io/FileDescriptor;I)V", (void*) android_net_util_setupRaSocket },
+    { "setupNaSocket", "(Ljava/io/FileDescriptor;)V",
+        (void*) android_net_util_setupNaSocket },
+    { "setupNsSocket", "(Ljava/io/FileDescriptor;)V",
+        (void*) android_net_util_setupNsSocket },
+    { "setupRaSocket", "(Ljava/io/FileDescriptor;I)V",
+        (void*) android_net_util_setupRaSocket },
 };
 
 int register_android_net_util_TetheringUtils(JNIEnv* env) {
diff --git a/Tethering/src/android/net/ip/DadProxy.java b/Tethering/src/android/net/ip/DadProxy.java
new file mode 100644
index 0000000..e2976b7
--- /dev/null
+++ b/Tethering/src/android/net/ip/DadProxy.java
@@ -0,0 +1,54 @@
+/*
+ * 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 android.net.ip;
+
+import android.net.util.InterfaceParams;
+import android.os.Handler;
+
+import androidx.annotation.VisibleForTesting;
+
+/**
+ * Basic Duplicate address detection proxy.
+ *
+ * @hide
+ */
+public class DadProxy {
+    private static final String TAG = DadProxy.class.getSimpleName();
+
+    @VisibleForTesting
+    public static NeighborPacketForwarder naForwarder;
+    public static NeighborPacketForwarder nsForwarder;
+
+    public DadProxy(Handler h, InterfaceParams tetheredIface) {
+        naForwarder = new NeighborPacketForwarder(h, tetheredIface,
+                                        NeighborPacketForwarder.ICMPV6_NEIGHBOR_ADVERTISEMENT);
+        nsForwarder = new NeighborPacketForwarder(h, tetheredIface,
+                                        NeighborPacketForwarder.ICMPV6_NEIGHBOR_SOLICITATION);
+    }
+
+    /** Stop NS/NA Forwarders. */
+    public void stop() {
+        naForwarder.stop();
+        nsForwarder.stop();
+    }
+
+    /** Set upstream iface on both forwarders. */
+    public void setUpstreamIface(InterfaceParams upstreamIface) {
+        naForwarder.setUpstreamIface(upstreamIface);
+        nsForwarder.setUpstreamIface(upstreamIface);
+    }
+}
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index 66f216b..52d59fc 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -51,6 +51,7 @@
 import android.net.util.InterfaceSet;
 import android.net.util.PrefixUtils;
 import android.net.util.SharedLog;
+import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
@@ -160,6 +161,15 @@
 
     /** Capture IpServer dependencies, for injection. */
     public abstract static class Dependencies {
+        /**
+         * Create a DadProxy instance to be used by IpServer.
+         * To support multiple tethered interfaces concurrently DAD Proxy
+         * needs to be supported per IpServer instead of per upstream.
+         */
+        public DadProxy getDadProxy(Handler handler, InterfaceParams ifParams) {
+            return new DadProxy(handler, ifParams);
+        }
+
         /** Create an IpNeighborMonitor to be used by this IpServer */
         public IpNeighborMonitor getIpNeighborMonitor(Handler handler, SharedLog log,
                 IpNeighborMonitor.NeighborEventConsumer consumer) {
@@ -256,6 +266,7 @@
     // Advertisements (otherwise, we do not add them to mLinkProperties at all).
     private LinkProperties mLastIPv6LinkProperties;
     private RouterAdvertisementDaemon mRaDaemon;
+    private DadProxy mDadProxy;
 
     // To be accessed only on the handler thread
     private int mDhcpServerStartIndex = 0;
@@ -674,6 +685,13 @@
             return false;
         }
 
+        // TODO: use ShimUtils instead of explicitly checking the version here.
+        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R || "S".equals(Build.VERSION.CODENAME)
+                    || "T".equals(Build.VERSION.CODENAME)) {
+            // DAD Proxy starts forwarding packets after IPv6 upstream is present.
+            mDadProxy = mDeps.getDadProxy(getHandler(), mInterfaceParams);
+        }
+
         return true;
     }
 
@@ -685,6 +703,11 @@
             mRaDaemon.stop();
             mRaDaemon = null;
         }
+
+        if (mDadProxy != null) {
+            mDadProxy.stop();
+            mDadProxy = null;
+        }
     }
 
     // IPv6TetheringCoordinator sends updates with carefully curated IPv6-only
@@ -702,11 +725,16 @@
         }
 
         RaParams params = null;
-        int upstreamIfindex = 0;
+        String upstreamIface = null;
+        InterfaceParams upstreamIfaceParams = null;
+        int upstreamIfIndex = 0;
 
         if (v6only != null) {
-            final String upstreamIface = v6only.getInterfaceName();
-
+            upstreamIface = v6only.getInterfaceName();
+            upstreamIfaceParams = mDeps.getInterfaceParams(upstreamIface);
+            if (upstreamIfaceParams != null) {
+                upstreamIfIndex = upstreamIfaceParams.index;
+            }
             params = new RaParams();
             params.mtu = v6only.getMtu();
             params.hasDefaultRoute = v6only.hasIpv6DefaultRoute();
@@ -726,15 +754,13 @@
                 }
             }
 
-            upstreamIfindex = mDeps.getIfindex(upstreamIface);
-
             // Add upstream index to name mapping for the tether stats usage in the coordinator.
             // Although this mapping could be added by both class Tethering and IpServer, adding
             // mapping from IpServer guarantees that the mapping is added before the adding
             // forwarding rules. That is because there are different state machines in both
             // classes. It is hard to guarantee the link property update order between multiple
             // state machines.
-            mBpfCoordinator.addUpstreamNameToLookupTable(upstreamIfindex, upstreamIface);
+            mBpfCoordinator.addUpstreamNameToLookupTable(upstreamIfIndex, upstreamIface);
         }
 
         // If v6only is null, we pass in null to setRaParams(), which handles
@@ -743,8 +769,11 @@
         setRaParams(params);
         mLastIPv6LinkProperties = v6only;
 
-        updateIpv6ForwardingRules(mLastIPv6UpstreamIfindex, upstreamIfindex, null);
-        mLastIPv6UpstreamIfindex = upstreamIfindex;
+        updateIpv6ForwardingRules(mLastIPv6UpstreamIfindex, upstreamIfIndex, null);
+        mLastIPv6UpstreamIfindex = upstreamIfIndex;
+        if (mDadProxy != null) {
+            mDadProxy.setUpstreamIface(upstreamIfaceParams);
+        }
     }
 
     private void removeRoutesFromLocalNetwork(@NonNull final List<RouteInfo> toBeRemoved) {
diff --git a/Tethering/src/android/net/ip/NeighborPacketForwarder.java b/Tethering/src/android/net/ip/NeighborPacketForwarder.java
new file mode 100644
index 0000000..084743d
--- /dev/null
+++ b/Tethering/src/android/net/ip/NeighborPacketForwarder.java
@@ -0,0 +1,181 @@
+/*
+ * 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 android.net.ip;
+
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.AF_PACKET;
+import static android.system.OsConstants.ETH_P_IPV6;
+import static android.system.OsConstants.IPPROTO_RAW;
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOCK_NONBLOCK;
+import static android.system.OsConstants.SOCK_RAW;
+
+import android.net.util.InterfaceParams;
+import android.net.util.SocketUtils;
+import android.net.util.TetheringUtils;
+import android.os.Handler;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import com.android.net.module.util.PacketReader;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.Inet6Address;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+
+/**
+ * Basic IPv6 Neighbor Advertisement Forwarder.
+ *
+ * Forward NA packets from upstream iface to tethered iface
+ * and NS packets from tethered iface to upstream iface.
+ *
+ * @hide
+ */
+public class NeighborPacketForwarder extends PacketReader {
+    private final String mTag;
+
+    private FileDescriptor mFd;
+
+    // TODO: get these from NetworkStackConstants.
+    private static final int IPV6_ADDR_LEN = 16;
+    private static final int IPV6_DST_ADDR_OFFSET = 24;
+    private static final int IPV6_HEADER_LEN = 40;
+    private static final int ETH_HEADER_LEN = 14;
+
+    private InterfaceParams mListenIfaceParams, mSendIfaceParams;
+
+    private final int mType;
+    public static final int ICMPV6_NEIGHBOR_ADVERTISEMENT  = 136;
+    public static final int ICMPV6_NEIGHBOR_SOLICITATION = 135;
+
+    public NeighborPacketForwarder(Handler h, InterfaceParams tetheredInterface, int type) {
+        super(h);
+        mTag = NeighborPacketForwarder.class.getSimpleName() + "-"
+                + tetheredInterface.name + "-" + type;
+        mType = type;
+
+        if (mType == ICMPV6_NEIGHBOR_ADVERTISEMENT) {
+            mSendIfaceParams = tetheredInterface;
+        } else {
+            mListenIfaceParams = tetheredInterface;
+        }
+    }
+
+    /** Set new upstream iface and start/stop based on new params. */
+    public void setUpstreamIface(InterfaceParams upstreamParams) {
+        final InterfaceParams oldUpstreamParams;
+
+        if (mType == ICMPV6_NEIGHBOR_ADVERTISEMENT) {
+            oldUpstreamParams = mListenIfaceParams;
+            mListenIfaceParams = upstreamParams;
+        } else {
+            oldUpstreamParams = mSendIfaceParams;
+            mSendIfaceParams = upstreamParams;
+        }
+
+        if (oldUpstreamParams == null && upstreamParams != null) {
+            start();
+        } else if (oldUpstreamParams != null && upstreamParams == null) {
+            stop();
+        } else if (oldUpstreamParams != null && upstreamParams != null
+                   && oldUpstreamParams.index != upstreamParams.index) {
+            stop();
+            start();
+        }
+    }
+
+    // TODO: move NetworkStackUtils.closeSocketQuietly to
+    // frameworks/libs/net/common/device/com/android/net/module/util/[someclass].
+    private void closeSocketQuietly(FileDescriptor fd) {
+        try {
+            SocketUtils.closeSocket(fd);
+        } catch (IOException ignored) {
+        }
+    }
+
+    @Override
+    protected FileDescriptor createFd() {
+        try {
+            // ICMPv6 packets from modem do not have eth header, so RAW socket cannot be used.
+            // To keep uniformity in both directions PACKET socket can be used.
+            mFd = Os.socket(AF_PACKET, SOCK_DGRAM | SOCK_NONBLOCK, 0);
+
+            // TODO: convert setup*Socket to setupICMPv6BpfFilter with filter type?
+            if (mType == ICMPV6_NEIGHBOR_ADVERTISEMENT) {
+                TetheringUtils.setupNaSocket(mFd);
+            } else if (mType == ICMPV6_NEIGHBOR_SOLICITATION) {
+                TetheringUtils.setupNsSocket(mFd);
+            }
+
+            SocketAddress bindAddress = SocketUtils.makePacketSocketAddress(
+                                                        ETH_P_IPV6, mListenIfaceParams.index);
+            Os.bind(mFd, bindAddress);
+        } catch (ErrnoException | SocketException e) {
+            Log.wtf(mTag, "Failed to create  socket", e);
+            closeSocketQuietly(mFd);
+            return null;
+        }
+
+        return mFd;
+    }
+
+    private Inet6Address getIpv6DestinationAddress(byte[] recvbuf) {
+        Inet6Address dstAddr;
+        try {
+            dstAddr = (Inet6Address) Inet6Address.getByAddress(Arrays.copyOfRange(recvbuf,
+                    IPV6_DST_ADDR_OFFSET, IPV6_DST_ADDR_OFFSET + IPV6_ADDR_LEN));
+        } catch (UnknownHostException | ClassCastException impossible) {
+            throw new AssertionError("16-byte array not valid IPv6 address?");
+        }
+        return dstAddr;
+    }
+
+    @Override
+    protected void handlePacket(byte[] recvbuf, int length) {
+        if (mSendIfaceParams == null) {
+            return;
+        }
+
+        // The BPF filter should already have checked the length of the packet, but...
+        if (length < IPV6_HEADER_LEN) {
+            return;
+        }
+        Inet6Address destv6 = getIpv6DestinationAddress(recvbuf);
+        if (!destv6.isMulticastAddress()) {
+            return;
+        }
+        InetSocketAddress dest = new InetSocketAddress(destv6, 0);
+
+        FileDescriptor fd = null;
+        try {
+            fd = Os.socket(AF_INET6, SOCK_RAW | SOCK_NONBLOCK, IPPROTO_RAW);
+            SocketUtils.bindSocketToInterface(fd, mSendIfaceParams.name);
+
+            int ret = Os.sendto(fd, recvbuf, 0, length, 0, dest);
+        } catch (ErrnoException | SocketException e) {
+            Log.e(mTag, "handlePacket error: " + e);
+        } finally {
+            closeSocketQuietly(fd);
+        }
+    }
+}
diff --git a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
index 6f017dc..7c0b7cc 100644
--- a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
+++ b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
@@ -18,6 +18,7 @@
 
 import static android.net.util.NetworkConstants.IPV6_MIN_MTU;
 import static android.net.util.NetworkConstants.RFC7421_PREFIX_LENGTH;
+import static android.net.util.TetheringUtils.getAllNodesForScopeId;
 import static android.system.OsConstants.AF_INET6;
 import static android.system.OsConstants.IPPROTO_ICMPV6;
 import static android.system.OsConstants.SOCK_RAW;
@@ -44,7 +45,6 @@
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.SocketException;
-import java.net.UnknownHostException;
 import java.nio.BufferOverflowException;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
@@ -92,10 +92,6 @@
 
     private static final int DAY_IN_SECONDS = 86_400;
 
-    private static final byte[] ALL_NODES = new byte[] {
-            (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
-    };
-
     private final InterfaceParams mInterface;
     private final InetSocketAddress mAllNodes;
 
@@ -240,7 +236,6 @@
         }
     }
 
-
     public RouterAdvertisementDaemon(InterfaceParams ifParams) {
         mInterface = ifParams;
         mAllNodes = new InetSocketAddress(getAllNodesForScopeId(mInterface.index), 0);
@@ -363,15 +358,6 @@
         }
     }
 
-    private static Inet6Address getAllNodesForScopeId(int scopeId) {
-        try {
-            return Inet6Address.getByAddress("ff02::1", ALL_NODES, scopeId);
-        } catch (UnknownHostException uhe) {
-            Log.wtf(TAG, "Failed to construct ff02::1 InetAddress: " + uhe);
-            return null;
-        }
-    }
-
     private static byte asByte(int value) {
         return (byte) value;
     }
diff --git a/Tethering/src/android/net/util/TetheringUtils.java b/Tethering/src/android/net/util/TetheringUtils.java
index b17b4ba..53b54f7 100644
--- a/Tethering/src/android/net/util/TetheringUtils.java
+++ b/Tethering/src/android/net/util/TetheringUtils.java
@@ -17,11 +17,15 @@
 
 import android.net.TetherStatsParcel;
 import android.net.TetheringRequestParcel;
+import android.util.Log;
 
 import androidx.annotation.NonNull;
 
 import java.io.FileDescriptor;
+import java.net.Inet6Address;
 import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.Arrays;
 import java.util.Objects;
 
 /**
@@ -30,6 +34,24 @@
  * {@hide}
  */
 public class TetheringUtils {
+    public static final byte[] ALL_NODES = new byte[] {
+        (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
+    };
+
+    /**
+     * Configures a socket for receiving and sending ICMPv6 neighbor advertisments.
+     * @param fd the socket's {@link FileDescriptor}.
+     */
+    public static native void setupNaSocket(FileDescriptor fd)
+            throws SocketException;
+
+    /**
+     * Configures a socket for receiving and sending ICMPv6 neighbor solicitations.
+     * @param fd the socket's {@link FileDescriptor}.
+     */
+    public static native void setupNsSocket(FileDescriptor fd)
+            throws SocketException;
+
     /**
      *  The object which records offload Tx/Rx forwarded bytes/packets.
      *  TODO: Replace the inner class ForwardedStats of class OffloadHardwareInterface with
@@ -129,4 +151,15 @@
                 && request.exemptFromEntitlementCheck == otherRequest.exemptFromEntitlementCheck
                 && request.showProvisioningUi == otherRequest.showProvisioningUi;
     }
+
+    /** Get inet6 address for all nodes given scope ID. */
+    public static Inet6Address getAllNodesForScopeId(int scopeId) {
+        try {
+            return Inet6Address.getByAddress("ff02::1", ALL_NODES, scopeId);
+        } catch (UnknownHostException uhe) {
+            Log.wtf("TetheringUtils", "Failed to construct Inet6Address from "
+                    + Arrays.toString(ALL_NODES) + " and scopedId " + scopeId);
+            return null;
+        }
+    }
 }
diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java b/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
index 33b9d00..da5f25b 100644
--- a/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
+++ b/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
@@ -28,6 +28,7 @@
 import android.hardware.tetheroffload.control.V1_0.NetworkProtocol;
 import android.hardware.tetheroffload.control.V1_0.OffloadCallbackEvent;
 import android.net.netlink.NetlinkSocket;
+import android.net.netlink.StructNfGenMsg;
 import android.net.netlink.StructNlMsgHdr;
 import android.net.util.SharedLog;
 import android.net.util.SocketUtils;
@@ -41,11 +42,12 @@
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.io.FileDescriptor;
-import java.io.InterruptedIOException;
 import java.io.IOException;
+import java.io.InterruptedIOException;
 import java.net.SocketAddress;
 import java.net.SocketException;
 import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 import java.util.ArrayList;
 import java.util.NoSuchElementException;
 
@@ -66,11 +68,12 @@
     private static final String NO_IPV4_ADDRESS = "";
     private static final String NO_IPV4_GATEWAY = "";
     // Reference kernel/uapi/linux/netfilter/nfnetlink_compat.h
-    private static final int NF_NETLINK_CONNTRACK_NEW = 1;
-    private static final int NF_NETLINK_CONNTRACK_UPDATE = 2;
-    private static final int NF_NETLINK_CONNTRACK_DESTROY = 4;
+    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;
     // Reference libnetfilter_conntrack/linux_nfnetlink_conntrack.h
     public static final short NFNL_SUBSYS_CTNETLINK = 1;
+    public static final short IPCTNL_MSG_CT_NEW = 0;
     public static final short IPCTNL_MSG_CT_GET = 1;
 
     private final long NETLINK_MESSAGE_TIMEOUT_MS = 500;
@@ -237,7 +240,7 @@
                 NF_NETLINK_CONNTRACK_NEW | NF_NETLINK_CONNTRACK_DESTROY);
         if (h1 == null) return false;
 
-        sendNetlinkMessage(h1, (short) ((NFNL_SUBSYS_CTNETLINK << 8) | IPCTNL_MSG_CT_GET),
+        sendIpv4NfGenMsg(h1, (short) ((NFNL_SUBSYS_CTNETLINK << 8) | IPCTNL_MSG_CT_GET),
                            (short) (NLM_F_REQUEST | NLM_F_DUMP));
 
         final NativeHandle h2 = mDeps.createConntrackSocket(
@@ -267,16 +270,23 @@
     }
 
     @VisibleForTesting
-    public void sendNetlinkMessage(@NonNull NativeHandle handle, short type, short flags) {
-        final int length = StructNlMsgHdr.STRUCT_SIZE;
+    public void sendIpv4NfGenMsg(@NonNull NativeHandle handle, short type, short flags) {
+        final int length = StructNlMsgHdr.STRUCT_SIZE + StructNfGenMsg.STRUCT_SIZE;
         final byte[] msg = new byte[length];
-        final StructNlMsgHdr nlh = new StructNlMsgHdr();
         final ByteBuffer byteBuffer = ByteBuffer.wrap(msg);
+        byteBuffer.order(ByteOrder.nativeOrder());
+
+        final StructNlMsgHdr nlh = new StructNlMsgHdr();
         nlh.nlmsg_len = length;
         nlh.nlmsg_type = type;
         nlh.nlmsg_flags = flags;
-        nlh.nlmsg_seq = 1;
+        nlh.nlmsg_seq = 0;
         nlh.pack(byteBuffer);
+
+        // Header needs to be added to buffer since a generic netlink request is being sent.
+        final StructNfGenMsg nfh = new StructNfGenMsg((byte) OsConstants.AF_INET);
+        nfh.pack(byteBuffer);
+
         try {
             NetlinkSocket.sendMessage(handle.getFileDescriptor(), msg, 0 /* offset */, length,
                                       NETLINK_MESSAGE_TIMEOUT_MS);
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 2c91d10..62ae88c 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -140,8 +140,8 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.Iterator;
-import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.Executor;
@@ -216,13 +216,12 @@
     private final ArrayMap<String, TetherState> mTetherStates;
     private final BroadcastReceiver mStateReceiver;
     private final Looper mLooper;
-    private final StateMachine mTetherMainSM;
+    private final TetherMainSM mTetherMainSM;
     private final OffloadController mOffloadController;
     private final UpstreamNetworkMonitor mUpstreamNetworkMonitor;
     // TODO: Figure out how to merge this and other downstream-tracking objects
     // into a single coherent structure.
-    // Use LinkedHashSet for predictable ordering order for ConnectedClientsTracker.
-    private final LinkedHashSet<IpServer> mForwardedDownstreams;
+    private final HashSet<IpServer> mForwardedDownstreams;
     private final VersionedBroadcastListener mCarrierConfigChange;
     private final TetheringDependencies mDeps;
     private final EntitlementManager mEntitlementMgr;
@@ -287,7 +286,7 @@
                 });
         mUpstreamNetworkMonitor = mDeps.getUpstreamNetworkMonitor(mContext, mTetherMainSM, mLog,
                 TetherMainSM.EVENT_UPSTREAM_CALLBACK);
-        mForwardedDownstreams = new LinkedHashSet<>();
+        mForwardedDownstreams = new HashSet<>();
 
         IntentFilter filter = new IntentFilter();
         filter.addAction(ACTION_CARRIER_CONFIG_CHANGED);
@@ -1423,6 +1422,7 @@
         //    interfaces.
         // 2) mNotifyList contains all state machines that may have outstanding tethering state
         //    that needs to be torn down.
+        // 3) Use mNotifyList for predictable ordering order for ConnectedClientsTracker.
         //
         // Because we excise interfaces immediately from mTetherStates, we must maintain mNotifyList
         // so that the garbage collector does not clean up the state machine before it has a chance
@@ -1459,6 +1459,15 @@
             setInitialState(mInitialState);
         }
 
+        /**
+         * Returns all downstreams that are serving clients, regardless of they are actually
+         * tethered or localOnly. This must be called on the tethering thread (not thread-safe).
+         */
+        @NonNull
+        public List<IpServer> getAllDownstreams() {
+            return mNotifyList;
+        }
+
         class InitialState extends State {
             @Override
             public boolean processMessage(Message message) {
@@ -2300,7 +2309,8 @@
     }
 
     private void updateConnectedClients(final List<WifiClient> wifiClients) {
-        if (mConnectedClientsTracker.updateConnectedClients(mForwardedDownstreams, wifiClients)) {
+        if (mConnectedClientsTracker.updateConnectedClients(mTetherMainSM.getAllDownstreams(),
+                wifiClients)) {
             reportTetherClientsChanged(mConnectedClientsTracker.getLastTetheredClients());
         }
     }
diff --git a/Tethering/tests/Android.bp b/Tethering/tests/Android.bp
new file mode 100644
index 0000000..731144c
--- /dev/null
+++ b/Tethering/tests/Android.bp
@@ -0,0 +1,24 @@
+//
+// 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.
+//
+
+filegroup {
+    name: "TetheringTestsJarJarRules",
+    srcs: ["jarjar-rules.txt"],
+    visibility: [
+        "//frameworks/base/packages/Tethering/tests:__subpackages__",
+        "//packages/modules/Connectivity/Tethering/tests:__subpackages__",
+    ]
+}
diff --git a/Tethering/tests/integration/Android.bp b/Tethering/tests/integration/Android.bp
index e0009dd..db503c6 100644
--- a/Tethering/tests/integration/Android.bp
+++ b/Tethering/tests/integration/Android.bp
@@ -22,7 +22,6 @@
     static_libs: [
         "NetworkStackApiStableLib",
         "androidx.test.rules",
-        "frameworks-base-testutils",
         "mockito-target-extended-minus-junit4",
         "net-tests-utils",
         "testables",
@@ -83,6 +82,7 @@
         // For NetworkStackUtils included in NetworkStackBase
         "libnetworkstackutilsjni",
     ],
+    jarjar_rules: ":TetheringTestsJarJarRules",
     compile_multilib: "both",
     manifest: "AndroidManifest_coverage.xml",
-}
\ No newline at end of file
+}
diff --git a/Tethering/tests/unit/jarjar-rules.txt b/Tethering/tests/jarjar-rules.txt
similarity index 69%
rename from Tethering/tests/unit/jarjar-rules.txt
rename to Tethering/tests/jarjar-rules.txt
index ec2d2b0..c99ff7f 100644
--- a/Tethering/tests/unit/jarjar-rules.txt
+++ b/Tethering/tests/jarjar-rules.txt
@@ -9,3 +9,11 @@
 rule com.android.internal.util.TrafficStatsConstants* com.android.networkstack.tethering.util.TrafficStatsConstants@1
 
 rule android.util.LocalLog* com.android.networkstack.tethering.util.LocalLog@1
+
+# Classes from net-utils-framework-common
+rule com.android.net.module.util.** com.android.networkstack.tethering.util.@1
+
+# TODO: either stop using frameworks-base-testutils or remove the unit test classes it contains.
+# TestableLooper from "testables" can be used instead of TestLooper from frameworks-base-testutils.
+zap android.os.test.TestLooperTest*
+zap com.android.test.filters.SelectTestTests*
diff --git a/Tethering/tests/privileged/Android.bp b/Tethering/tests/privileged/Android.bp
new file mode 100644
index 0000000..9217345
--- /dev/null
+++ b/Tethering/tests/privileged/Android.bp
@@ -0,0 +1,49 @@
+//
+// 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.
+//
+
+java_defaults {
+    name: "TetheringPrivilegedTestsJniDefaults",
+    jni_libs: [
+        "libdexmakerjvmtiagent",
+        "libstaticjvmtiagent",
+        "libtetherutilsjni",
+    ],
+    jni_uses_sdk_apis: true,
+    visibility: ["//visibility:private"],
+}
+
+android_test {
+    name: "TetheringPrivilegedTests",
+    defaults: [
+        "TetheringPrivilegedTestsJniDefaults",
+    ],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
+    certificate: "networkstack",
+    platform_apis: true,
+    test_suites: [
+        "device-tests",
+        "mts",
+    ],
+    static_libs: [
+        "androidx.test.rules",
+        "net-tests-utils",
+        "TetheringApiCurrentLib",
+    ],
+    compile_multilib: "both",
+}
diff --git a/Tethering/tests/privileged/AndroidManifest.xml b/Tethering/tests/privileged/AndroidManifest.xml
new file mode 100644
index 0000000..49eba15
--- /dev/null
+++ b/Tethering/tests/privileged/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.networkstack.tethering.tests.privileged"
+          android:sharedUserId="android.uid.networkstack">
+
+    <!-- Note: do not add any privileged or signature permissions that are granted
+         to the network stack and its shared uid apps. Otherwise, the test APK will
+         install, but when the device is rebooted, it will bootloop because this
+         test APK is not in the privileged permission allow list -->
+
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.networkstack.tethering.tests.privileged"
+        android:label="Tethering privileged tests">
+    </instrumentation>
+</manifest>
diff --git a/Tethering/tests/privileged/src/android/net/ip/DadProxyTest.java b/Tethering/tests/privileged/src/android/net/ip/DadProxyTest.java
new file mode 100644
index 0000000..747d3e8
--- /dev/null
+++ b/Tethering/tests/privileged/src/android/net/ip/DadProxyTest.java
@@ -0,0 +1,338 @@
+/*
+ * 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 android.net.ip;
+
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.system.OsConstants.IPPROTO_TCP;
+
+import static com.android.internal.util.BitUtils.uint16;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.net.INetd;
+import android.net.InetAddresses;
+import android.net.MacAddress;
+import android.net.TestNetworkInterface;
+import android.net.TestNetworkManager;
+import android.net.util.InterfaceParams;
+import android.net.util.IpUtils;
+import android.net.util.TetheringUtils;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.TapPacketReader;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+
+import java.io.FileDescriptor;
+import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicReference;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DadProxyTest {
+    private static final int DATA_BUFFER_LEN = 4096;
+    private static final int PACKET_TIMEOUT_MS = 5_000;
+
+    // TODO: make NetworkStackConstants accessible to this test and use the constant from there.
+    private static final int ETHER_SRC_ADDR_OFFSET = 6;
+
+    private DadProxy mProxy;
+    TestNetworkInterface mUpstreamTestIface, mTetheredTestIface;
+    private InterfaceParams mUpstreamParams, mTetheredParams;
+    private HandlerThread mHandlerThread;
+    private Handler mHandler;
+    private TapPacketReader mUpstreamPacketReader, mTetheredPacketReader;
+    private FileDescriptor mUpstreamTapFd, mTetheredTapFd;
+
+    private static INetd sNetd;
+
+    @BeforeClass
+    public static void setupOnce() {
+        System.loadLibrary("tetherutilsjni");
+
+        final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
+        final IBinder netdIBinder =
+                (IBinder) inst.getContext().getSystemService(Context.NETD_SERVICE);
+        sNetd = INetd.Stub.asInterface(netdIBinder);
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mHandlerThread = new HandlerThread(getClass().getSimpleName());
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+
+        setupTapInterfaces();
+
+        // Looper must be prepared here since AndroidJUnitRunner runs tests on separate threads.
+        if (Looper.myLooper() == null) Looper.prepare();
+
+        DadProxy mProxy = setupProxy();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mHandlerThread != null) {
+            mHandler.post(mUpstreamPacketReader::stop); // Also closes the socket
+            mHandler.post(mTetheredPacketReader::stop); // Also closes the socket
+            mUpstreamTapFd = null;
+            mTetheredTapFd = null;
+            mHandlerThread.quitSafely();
+        }
+
+        if (mTetheredParams != null) {
+            sNetd.networkRemoveInterface(INetd.LOCAL_NET_ID, mTetheredParams.name);
+        }
+        if (mUpstreamParams != null) {
+            sNetd.networkRemoveInterface(INetd.LOCAL_NET_ID, mUpstreamParams.name);
+        }
+
+        if (mUpstreamTestIface != null) {
+            try {
+                Os.close(mUpstreamTestIface.getFileDescriptor().getFileDescriptor());
+            } catch (ErrnoException e) { }
+        }
+
+        if (mTetheredTestIface != null) {
+            try {
+                Os.close(mTetheredTestIface.getFileDescriptor().getFileDescriptor());
+            } catch (ErrnoException e) { }
+        }
+    }
+
+    private TestNetworkInterface setupTapInterface() {
+        final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
+        AtomicReference<TestNetworkInterface> iface = new AtomicReference<>();
+
+        inst.getUiAutomation().adoptShellPermissionIdentity();
+        try {
+            final TestNetworkManager tnm = (TestNetworkManager) inst.getContext().getSystemService(
+                    Context.TEST_NETWORK_SERVICE);
+            iface.set(tnm.createTapInterface());
+        } finally {
+            inst.getUiAutomation().dropShellPermissionIdentity();
+        }
+
+        return iface.get();
+    }
+
+    private void setupTapInterfaces() {
+        // Create upstream test iface.
+        mUpstreamTestIface = setupTapInterface();
+        mUpstreamParams = InterfaceParams.getByName(mUpstreamTestIface.getInterfaceName());
+        assertNotNull(mUpstreamParams);
+        mUpstreamTapFd = mUpstreamTestIface.getFileDescriptor().getFileDescriptor();
+        mUpstreamPacketReader = new TapPacketReader(mHandler, mUpstreamTapFd,
+                                                    DATA_BUFFER_LEN);
+        mHandler.post(mUpstreamPacketReader::start);
+
+        // Create tethered test iface.
+        mTetheredTestIface = setupTapInterface();
+        mTetheredParams = InterfaceParams.getByName(mTetheredTestIface.getInterfaceName());
+        assertNotNull(mTetheredParams);
+        mTetheredTapFd = mTetheredTestIface.getFileDescriptor().getFileDescriptor();
+        mTetheredPacketReader = new TapPacketReader(mHandler, mTetheredTapFd,
+                                                    DATA_BUFFER_LEN);
+        mHandler.post(mTetheredPacketReader::start);
+    }
+
+    private static final int IPV6_HEADER_LEN = 40;
+    private static final int ETH_HEADER_LEN = 14;
+    private static final int ICMPV6_NA_NS_LEN = 24;
+    private static final int LL_TARGET_OPTION_LEN = 8;
+    private static final int ICMPV6_CHECKSUM_OFFSET = 2;
+    private static final int ETHER_TYPE_IPV6 = 0x86dd;
+
+    // TODO: move the IpUtils code to frameworks/lib/net and link it statically.
+    private static int checksumFold(int sum) {
+        while (sum > 0xffff) {
+            sum = (sum >> 16) + (sum & 0xffff);
+        }
+        return sum;
+    }
+
+    // TODO: move the IpUtils code to frameworks/lib/net and link it statically.
+    private static short checksumAdjust(short checksum, short oldWord, short newWord) {
+        checksum = (short) ~checksum;
+        int tempSum = checksumFold(uint16(checksum) + uint16(newWord) + 0xffff - uint16(oldWord));
+        return (short) ~tempSum;
+    }
+
+    // TODO: move the IpUtils code to frameworks/lib/net and link it statically.
+    private static short icmpv6Checksum(ByteBuffer buf, int ipOffset, int transportOffset,
+            int transportLen) {
+        // The ICMPv6 checksum is the same as the TCP checksum, except the pseudo-header uses
+        // 58 (ICMPv6) instead of 6 (TCP). Calculate the TCP checksum, and then do an incremental
+        // checksum adjustment  for the change in the next header byte.
+        short checksum = IpUtils.tcpChecksum(buf, ipOffset, transportOffset, transportLen);
+        return checksumAdjust(checksum, (short) IPPROTO_TCP, (short) IPPROTO_ICMPV6);
+    }
+
+    private static ByteBuffer createDadPacket(int type) {
+        // Refer to buildArpPacket()
+        int icmpLen = ICMPV6_NA_NS_LEN
+                + (type == NeighborPacketForwarder.ICMPV6_NEIGHBOR_ADVERTISEMENT
+                ? LL_TARGET_OPTION_LEN : 0);
+        final ByteBuffer buf = ByteBuffer.allocate(icmpLen + IPV6_HEADER_LEN + ETH_HEADER_LEN);
+
+        // Ethernet header.
+        final MacAddress srcMac = MacAddress.fromString("33:33:ff:66:77:88");
+        buf.put(srcMac.toByteArray());
+        final MacAddress dstMac = MacAddress.fromString("01:02:03:04:05:06");
+        buf.put(dstMac.toByteArray());
+        buf.putShort((short) ETHER_TYPE_IPV6);
+
+        // IPv6 header
+        byte[] version = {(byte) 0x60, 0x00, 0x00, 0x00};
+        buf.put(version);                                           // Version
+        buf.putShort((byte) icmpLen);                               // Length
+        buf.put((byte) IPPROTO_ICMPV6);                             // Next header
+        buf.put((byte) 0xff);                                       // Hop limit
+
+        final byte[] target =
+            InetAddresses.parseNumericAddress("fe80::1122:3344:5566:7788").getAddress();
+        final byte[] src;
+        final byte[] dst;
+        if (type == NeighborPacketForwarder.ICMPV6_NEIGHBOR_SOLICITATION) {
+            src = InetAddresses.parseNumericAddress("::").getAddress();
+            dst = InetAddresses.parseNumericAddress("ff02::1:ff66:7788").getAddress();
+        } else {
+            src = target;
+            dst = TetheringUtils.ALL_NODES;
+        }
+        buf.put(src);
+        buf.put(dst);
+
+        // ICMPv6 Header
+        buf.put((byte) type);                                       // Type
+        buf.put((byte) 0x00);                                       // Code
+        buf.putShort((short) 0);                                    // Checksum
+        buf.putInt(0);                                              // Reserved
+        buf.put(target);
+
+        if (type == NeighborPacketForwarder.ICMPV6_NEIGHBOR_ADVERTISEMENT) {
+            //NA packet has LL target address
+            //ICMPv6 Option
+            buf.put((byte) 0x02);                                   // Type
+            buf.put((byte) 0x01);                                   // Length
+            byte[] ll_target = MacAddress.fromString("01:02:03:04:05:06").toByteArray();
+            buf.put(ll_target);
+        }
+
+        // Populate checksum field
+        final int transportOffset = ETH_HEADER_LEN + IPV6_HEADER_LEN;
+        final short checksum = icmpv6Checksum(buf, ETH_HEADER_LEN, transportOffset, icmpLen);
+        buf.putShort(transportOffset + ICMPV6_CHECKSUM_OFFSET, checksum);
+
+        buf.flip();
+        return buf;
+    }
+
+    private DadProxy setupProxy() throws Exception {
+        DadProxy proxy = new DadProxy(mHandler, mTetheredParams);
+        mHandler.post(() -> proxy.setUpstreamIface(mUpstreamParams));
+
+        // Upstream iface is added to local network to simplify test case.
+        // Otherwise the test needs to create and destroy a network for the upstream iface.
+        sNetd.networkAddInterface(INetd.LOCAL_NET_ID, mUpstreamParams.name);
+        sNetd.networkAddInterface(INetd.LOCAL_NET_ID, mTetheredParams.name);
+
+        return proxy;
+    }
+
+    // TODO: change to assert.
+    private boolean waitForPacket(ByteBuffer packet, TapPacketReader reader) {
+        byte[] p;
+
+        while ((p = reader.popPacket(PACKET_TIMEOUT_MS)) != null) {
+            final ByteBuffer buffer = ByteBuffer.wrap(p);
+
+            if (buffer.compareTo(packet) == 0) return true;
+        }
+        return false;
+    }
+
+    private void updateDstMac(ByteBuffer buf, MacAddress mac) {
+        buf.put(mac.toByteArray());
+        buf.rewind();
+    }
+    private void updateSrcMac(ByteBuffer buf, InterfaceParams ifaceParams) {
+        buf.position(ETHER_SRC_ADDR_OFFSET);
+        buf.put(ifaceParams.macAddr.toByteArray());
+        buf.rewind();
+    }
+
+    @Test
+    public void testNaForwardingFromUpstreamToTether() throws Exception {
+        ByteBuffer na = createDadPacket(NeighborPacketForwarder.ICMPV6_NEIGHBOR_ADVERTISEMENT);
+
+        mUpstreamPacketReader.sendResponse(na);
+        updateDstMac(na, MacAddress.fromString("33:33:00:00:00:01"));
+        updateSrcMac(na, mTetheredParams);
+        assertTrue(waitForPacket(na, mTetheredPacketReader));
+    }
+
+    @Test
+    // TODO: remove test once DAD works in both directions.
+    public void testNaForwardingFromTetherToUpstream() throws Exception {
+        ByteBuffer na = createDadPacket(NeighborPacketForwarder.ICMPV6_NEIGHBOR_ADVERTISEMENT);
+
+        mTetheredPacketReader.sendResponse(na);
+        updateDstMac(na, MacAddress.fromString("33:33:00:00:00:01"));
+        updateSrcMac(na, mTetheredParams);
+        assertFalse(waitForPacket(na, mUpstreamPacketReader));
+    }
+
+    @Test
+    public void testNsForwardingFromTetherToUpstream() throws Exception {
+        ByteBuffer ns = createDadPacket(NeighborPacketForwarder.ICMPV6_NEIGHBOR_SOLICITATION);
+
+        mTetheredPacketReader.sendResponse(ns);
+        updateSrcMac(ns, mUpstreamParams);
+        assertTrue(waitForPacket(ns, mUpstreamPacketReader));
+    }
+
+    @Test
+    // TODO: remove test once DAD works in both directions.
+    public void testNsForwardingFromUpstreamToTether() throws Exception {
+        ByteBuffer ns = createDadPacket(NeighborPacketForwarder.ICMPV6_NEIGHBOR_SOLICITATION);
+
+        mUpstreamPacketReader.sendResponse(ns);
+        updateSrcMac(ns, mUpstreamParams);
+        assertFalse(waitForPacket(ns, mTetheredPacketReader));
+    }
+}
diff --git a/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java b/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
new file mode 100644
index 0000000..57c28fc
--- /dev/null
+++ b/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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.networkstack.tethering;
+
+import static android.net.netlink.NetlinkSocket.DEFAULT_RECV_BUFSIZE;
+import static android.net.netlink.StructNlMsgHdr.NLM_F_DUMP;
+import static android.net.netlink.StructNlMsgHdr.NLM_F_REQUEST;
+
+import static com.android.networkstack.tethering.OffloadHardwareInterface.IPCTNL_MSG_CT_GET;
+import static com.android.networkstack.tethering.OffloadHardwareInterface.IPCTNL_MSG_CT_NEW;
+import static com.android.networkstack.tethering.OffloadHardwareInterface.NFNL_SUBSYS_CTNETLINK;
+import static com.android.networkstack.tethering.OffloadHardwareInterface.NF_NETLINK_CONNTRACK_DESTROY;
+import static com.android.networkstack.tethering.OffloadHardwareInterface.NF_NETLINK_CONNTRACK_NEW;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.net.netlink.StructNlMsgHdr;
+import android.net.util.SharedLog;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.NativeHandle;
+import android.system.Os;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ConntrackSocketTest {
+    private static final long TIMEOUT = 500;
+
+    private HandlerThread mHandlerThread;
+    private Handler mHandler;
+    private final SharedLog mLog = new SharedLog("privileged-test");
+
+    private OffloadHardwareInterface mOffloadHw;
+    private OffloadHardwareInterface.Dependencies mDeps;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mHandlerThread = new HandlerThread(getClass().getSimpleName());
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+
+        // Looper must be prepared here since AndroidJUnitRunner runs tests on separate threads.
+        if (Looper.myLooper() == null) Looper.prepare();
+
+        mDeps = new OffloadHardwareInterface.Dependencies(mLog);
+        mOffloadHw = new OffloadHardwareInterface(mHandler, mLog, mDeps);
+    }
+
+    @Test
+    public void testIpv4ConntrackSocket() throws Exception {
+        // Set up server and connect.
+        final InetSocketAddress anyAddress = new InetSocketAddress(
+                InetAddress.getByName("127.0.0.1"), 0);
+        final ServerSocket serverSocket = new ServerSocket();
+        serverSocket.bind(anyAddress);
+        final SocketAddress theAddress = serverSocket.getLocalSocketAddress();
+
+        // Make a connection to the server.
+        final Socket socket = new Socket();
+        socket.connect(theAddress);
+        final Socket acceptedSocket = serverSocket.accept();
+
+        final NativeHandle handle = mDeps.createConntrackSocket(
+                NF_NETLINK_CONNTRACK_NEW | NF_NETLINK_CONNTRACK_DESTROY);
+        mOffloadHw.sendIpv4NfGenMsg(handle,
+                (short) ((NFNL_SUBSYS_CTNETLINK << 8) | IPCTNL_MSG_CT_GET),
+                (short) (NLM_F_REQUEST | NLM_F_DUMP));
+
+        boolean foundConntrackEntry = false;
+        ByteBuffer buffer = ByteBuffer.allocate(DEFAULT_RECV_BUFSIZE);
+        buffer.order(ByteOrder.nativeOrder());
+
+        try {
+            while (Os.read(handle.getFileDescriptor(), buffer) > 0) {
+                buffer.flip();
+
+                // TODO: ConntrackMessage should get a parse API like StructNlMsgHdr
+                // so we can confirm that the conntrack added is for the TCP connection above.
+                final StructNlMsgHdr nlmsghdr = StructNlMsgHdr.parse(buffer);
+                assertNotNull(nlmsghdr);
+
+                // As long as 1 conntrack entry is found test case will pass, even if it's not
+                // the from the TCP connection above.
+                if (nlmsghdr.nlmsg_type == ((NFNL_SUBSYS_CTNETLINK << 8) | IPCTNL_MSG_CT_NEW)) {
+                    foundConntrackEntry = true;
+                    break;
+                }
+            }
+        } finally {
+            socket.close();
+            serverSocket.close();
+        }
+        assertTrue("Did not receive any NFNL_SUBSYS_CTNETLINK/IPCTNL_MSG_CT_NEW message",
+                foundConntrackEntry);
+    }
+}
diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp
index 637a6b6..aabaa65 100644
--- a/Tethering/tests/unit/Android.bp
+++ b/Tethering/tests/unit/Android.bp
@@ -70,7 +70,6 @@
         "libdexmakerjvmtiagent",
         "libstaticjvmtiagent",
     ],
-    jarjar_rules: "jarjar-rules.txt",
 }
 
 // Library containing the unit tests. This is used by the coverage test target to pull in the
@@ -91,6 +90,7 @@
         "device-tests",
         "mts",
     ],
+    jarjar_rules: ":TetheringTestsJarJarRules",
     defaults: ["TetheringTestsDefaults"],
     compile_multilib: "both",
 }
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index 2d9f8ed..2eb7589 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -87,6 +87,7 @@
 import android.net.util.InterfaceSet;
 import android.net.util.PrefixUtils;
 import android.net.util.SharedLog;
+import android.os.Build;
 import android.os.Handler;
 import android.os.RemoteException;
 import android.os.test.TestLooper;
@@ -101,8 +102,12 @@
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
 import com.android.networkstack.tethering.PrivateAddressCoordinator;
 import com.android.networkstack.tethering.TetheringConfiguration;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -121,6 +126,9 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class IpServerTest {
+    @Rule
+    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
     private static final String IFACE_NAME = "testnet1";
     private static final String UPSTREAM_IFACE = "upstream0";
     private static final String UPSTREAM_IFACE2 = "upstream1";
@@ -133,6 +141,11 @@
 
     private static final InterfaceParams TEST_IFACE_PARAMS = new InterfaceParams(
             IFACE_NAME, 42 /* index */, MacAddress.ALL_ZEROS_ADDRESS, 1500 /* defaultMtu */);
+    private static final InterfaceParams UPSTREAM_IFACE_PARAMS = new InterfaceParams(
+            UPSTREAM_IFACE, UPSTREAM_IFINDEX, MacAddress.ALL_ZEROS_ADDRESS, 1500 /* defaultMtu */);
+    private static final InterfaceParams UPSTREAM_IFACE_PARAMS2 = new InterfaceParams(
+            UPSTREAM_IFACE2, UPSTREAM_IFINDEX2, MacAddress.ALL_ZEROS_ADDRESS,
+            1500 /* defaultMtu */);
 
     private static final int MAKE_DHCPSERVER_TIMEOUT_MS = 1000;
 
@@ -143,6 +156,7 @@
     @Mock private IpServer.Callback mCallback;
     @Mock private SharedLog mSharedLog;
     @Mock private IDhcpServer mDhcpServer;
+    @Mock private DadProxy mDadProxy;
     @Mock private RouterAdvertisementDaemon mRaDaemon;
     @Mock private IpNeighborMonitor mIpNeighborMonitor;
     @Mock private IpServer.Dependencies mDependencies;
@@ -166,8 +180,11 @@
 
     private void initStateMachine(int interfaceType, boolean usingLegacyDhcp,
             boolean usingBpfOffload) throws Exception {
+        when(mDependencies.getDadProxy(any(), any())).thenReturn(mDadProxy);
         when(mDependencies.getRouterAdvertisementDaemon(any())).thenReturn(mRaDaemon);
         when(mDependencies.getInterfaceParams(IFACE_NAME)).thenReturn(TEST_IFACE_PARAMS);
+        when(mDependencies.getInterfaceParams(UPSTREAM_IFACE)).thenReturn(UPSTREAM_IFACE_PARAMS);
+        when(mDependencies.getInterfaceParams(UPSTREAM_IFACE2)).thenReturn(UPSTREAM_IFACE_PARAMS2);
 
         when(mDependencies.getIfindex(eq(UPSTREAM_IFACE))).thenReturn(UPSTREAM_IFINDEX);
         when(mDependencies.getIfindex(eq(UPSTREAM_IFACE2))).thenReturn(UPSTREAM_IFINDEX2);
@@ -1107,4 +1124,78 @@
         }
         return true;
     }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void dadProxyUpdates() throws Exception {
+        InOrder inOrder = inOrder(mDadProxy);
+        initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE);
+        inOrder.verify(mDadProxy).setUpstreamIface(UPSTREAM_IFACE_PARAMS);
+
+        // Add an upstream without IPv6.
+        dispatchTetherConnectionChanged(UPSTREAM_IFACE, null, 0);
+        inOrder.verify(mDadProxy).setUpstreamIface(null);
+
+        // Add IPv6 to the upstream.
+        LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName(UPSTREAM_IFACE);
+        dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, 0);
+        inOrder.verify(mDadProxy).setUpstreamIface(UPSTREAM_IFACE_PARAMS);
+
+        // Change upstream.
+        // New linkproperties is needed, otherwise changing the iface has no impact.
+        LinkProperties lp2 = new LinkProperties();
+        lp2.setInterfaceName(UPSTREAM_IFACE2);
+        dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp2, 0);
+        inOrder.verify(mDadProxy).setUpstreamIface(UPSTREAM_IFACE_PARAMS2);
+
+        // Lose IPv6 on the upstream...
+        dispatchTetherConnectionChanged(UPSTREAM_IFACE2, null, 0);
+        inOrder.verify(mDadProxy).setUpstreamIface(null);
+
+        // ... and regain it on a different upstream.
+        dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, 0);
+        inOrder.verify(mDadProxy).setUpstreamIface(UPSTREAM_IFACE_PARAMS);
+
+        // Lose upstream.
+        dispatchTetherConnectionChanged(null, null, 0);
+        inOrder.verify(mDadProxy).setUpstreamIface(null);
+
+        // Regain upstream.
+        dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, 0);
+        inOrder.verify(mDadProxy).setUpstreamIface(UPSTREAM_IFACE_PARAMS);
+
+        // Stop tethering.
+        mIpServer.stop();
+        mLooper.dispatchAll();
+    }
+
+    private void checkDadProxyEnabled(boolean expectEnabled) throws Exception {
+        initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE);
+        InOrder inOrder = inOrder(mDadProxy);
+        // Add IPv6 to the upstream.
+        LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName(UPSTREAM_IFACE);
+        if (expectEnabled) {
+            inOrder.verify(mDadProxy).setUpstreamIface(UPSTREAM_IFACE_PARAMS);
+        } else {
+            inOrder.verifyNoMoreInteractions();
+        }
+        // Stop tethering.
+        mIpServer.stop();
+        mLooper.dispatchAll();
+        if (expectEnabled) {
+            inOrder.verify(mDadProxy).stop();
+        }
+        else {
+            verify(mDependencies, never()).getDadProxy(any(), any());
+        }
+    }
+    @Test @IgnoreAfter(Build.VERSION_CODES.R)
+    public void testDadProxyUpdates_DisabledUpToR() throws Exception {
+        checkDadProxyEnabled(false);
+    }
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testDadProxyUpdates_EnabledAfterR() throws Exception {
+        checkDadProxyEnabled(true);
+    }
 }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java
index c543fad..38b19dd 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java
@@ -17,8 +17,9 @@
 package com.android.networkstack.tethering;
 
 import static android.net.util.TetheringUtils.uint16;
-import static android.system.OsConstants.SOCK_STREAM;
+import static android.system.OsConstants.AF_INET;
 import static android.system.OsConstants.AF_UNIX;
+import static android.system.OsConstants.SOCK_STREAM;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -35,14 +36,15 @@
 import android.hardware.tetheroffload.control.V1_0.NatTimeoutUpdate;
 import android.hardware.tetheroffload.control.V1_0.NetworkProtocol;
 import android.hardware.tetheroffload.control.V1_0.OffloadCallbackEvent;
+import android.net.netlink.StructNfGenMsg;
 import android.net.netlink.StructNlMsgHdr;
 import android.net.util.SharedLog;
 import android.os.Handler;
 import android.os.NativeHandle;
 import android.os.test.TestLooper;
 import android.system.ErrnoException;
-import android.system.OsConstants;
 import android.system.Os;
+import android.system.OsConstants;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -55,8 +57,8 @@
 import org.mockito.MockitoAnnotations;
 
 import java.io.FileDescriptor;
-import java.io.OutputStream;
 import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 import java.util.ArrayList;
 
 @RunWith(AndroidJUnit4.class)
@@ -218,7 +220,7 @@
     }
 
     @Test
-    public void testNetlinkMessage() throws Exception {
+    public void testSendIpv4NfGenMsg() throws Exception {
         FileDescriptor writeSocket = new FileDescriptor();
         FileDescriptor readSocket = new FileDescriptor();
         try {
@@ -229,17 +231,25 @@
         }
         when(mNativeHandle.getFileDescriptor()).thenReturn(writeSocket);
 
-        mOffloadHw.sendNetlinkMessage(mNativeHandle, TEST_TYPE, TEST_FLAGS);
+        mOffloadHw.sendIpv4NfGenMsg(mNativeHandle, TEST_TYPE, TEST_FLAGS);
 
-        ByteBuffer buffer = ByteBuffer.allocate(StructNlMsgHdr.STRUCT_SIZE);
+        ByteBuffer buffer = ByteBuffer.allocate(9823);  // Arbitrary value > expectedLen.
+        buffer.order(ByteOrder.nativeOrder());
+
         int read = Os.read(readSocket, buffer);
+        final int expectedLen = StructNlMsgHdr.STRUCT_SIZE + StructNfGenMsg.STRUCT_SIZE;
+        assertEquals(expectedLen, read);
 
         buffer.flip();
-        assertEquals(StructNlMsgHdr.STRUCT_SIZE, buffer.getInt());
+        assertEquals(expectedLen, buffer.getInt());
         assertEquals(TEST_TYPE, buffer.getShort());
         assertEquals(TEST_FLAGS, buffer.getShort());
-        assertEquals(1 /* seq */, buffer.getInt());
+        assertEquals(0 /* seq */, buffer.getInt());
         assertEquals(0 /* pid */, buffer.getInt());
+        assertEquals(AF_INET, buffer.get());             // nfgen_family
+        assertEquals(0 /* error */, buffer.get());       // version
+        assertEquals(0 /* error */, buffer.getShort());  // res_id
+        assertEquals(expectedLen, buffer.position());
     }
 
     private NatTimeoutUpdate buildNatTimeoutUpdate(final int proto) {
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index 03c1a19..bdc695c 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -16,6 +16,7 @@
 
 package com.android.networkstack.tethering;
 
+import static android.Manifest.permission.NETWORK_SETTINGS;
 import static android.content.pm.PackageManager.GET_ACTIVITIES;
 import static android.hardware.usb.UsbManager.USB_CONFIGURED;
 import static android.hardware.usb.UsbManager.USB_CONNECTED;
@@ -36,6 +37,7 @@
 import static android.net.TetheringManager.TETHERING_NCM;
 import static android.net.TetheringManager.TETHERING_USB;
 import static android.net.TetheringManager.TETHERING_WIFI;
+import static android.net.TetheringManager.TETHERING_WIFI_P2P;
 import static android.net.TetheringManager.TETHER_ERROR_IFACE_CFG_ERROR;
 import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
 import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_IFACE;
@@ -49,12 +51,15 @@
 import static android.net.wifi.WifiManager.IFACE_IP_MODE_LOCAL_ONLY;
 import static android.net.wifi.WifiManager.IFACE_IP_MODE_TETHERED;
 import static android.net.wifi.WifiManager.WIFI_AP_STATE_ENABLED;
+import static android.system.OsConstants.RT_SCOPE_UNIVERSE;
 import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
 
+import static com.android.net.module.util.Inet4AddressUtils.inet4AddressToIntHTH;
 import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
 import static com.android.networkstack.tethering.Tethering.UserRestrictionActionListener;
 import static com.android.networkstack.tethering.TetheringNotificationUpdater.DOWNSTREAM_NONE;
 import static com.android.networkstack.tethering.UpstreamNetworkMonitor.EVENT_ON_CAPABILITIES;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
@@ -108,12 +113,16 @@
 import android.net.RouteInfo;
 import android.net.TetherStatesParcel;
 import android.net.TetheredClient;
+import android.net.TetheredClient.AddressInfo;
 import android.net.TetheringCallbackStartedParcel;
 import android.net.TetheringConfigurationParcel;
 import android.net.TetheringRequestParcel;
+import android.net.dhcp.DhcpLeaseParcelable;
 import android.net.dhcp.DhcpServerCallbacks;
 import android.net.dhcp.DhcpServingParamsParcel;
+import android.net.dhcp.IDhcpEventCallbacks;
 import android.net.dhcp.IDhcpServer;
+import android.net.ip.DadProxy;
 import android.net.ip.IpNeighborMonitor;
 import android.net.ip.IpServer;
 import android.net.ip.RouterAdvertisementDaemon;
@@ -121,7 +130,9 @@
 import android.net.util.NetworkConstants;
 import android.net.util.SharedLog;
 import android.net.wifi.SoftApConfiguration;
+import android.net.wifi.WifiClient;
 import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.SoftApCallback;
 import android.net.wifi.p2p.WifiP2pGroup;
 import android.net.wifi.p2p.WifiP2pInfo;
 import android.net.wifi.p2p.WifiP2pManager;
@@ -166,6 +177,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.Vector;
 
@@ -201,6 +213,7 @@
     @Mock private CarrierConfigManager mCarrierConfigManager;
     @Mock private UpstreamNetworkMonitor mUpstreamNetworkMonitor;
     @Mock private IPv6TetheringCoordinator mIPv6TetheringCoordinator;
+    @Mock private DadProxy mDadProxy;
     @Mock private RouterAdvertisementDaemon mRouterAdvertisementDaemon;
     @Mock private IpNeighborMonitor mIpNeighborMonitor;
     @Mock private IDhcpServer mDhcpServer;
@@ -234,6 +247,7 @@
     private EntitlementManager mEntitleMgr;
     private OffloadController mOffloadCtrl;
     private PrivateAddressCoordinator mPrivateAddressCoordinator;
+    private SoftApCallback mSoftApCallback;
 
     private class TestContext extends BroadcastInterceptingContext {
         TestContext(Context base) {
@@ -286,6 +300,12 @@
 
     public class MockIpServerDependencies extends IpServer.Dependencies {
         @Override
+        public DadProxy getDadProxy(
+                Handler handler, InterfaceParams ifParams) {
+            return mDadProxy;
+        }
+
+        @Override
         public RouterAdvertisementDaemon getRouterAdvertisementDaemon(
                 InterfaceParams ifParams) {
             return mRouterAdvertisementDaemon;
@@ -559,8 +579,12 @@
                 ArgumentCaptor.forClass(PhoneStateListener.class);
         verify(mTelephonyManager).listen(phoneListenerCaptor.capture(),
                 eq(PhoneStateListener.LISTEN_ACTIVE_DATA_SUBSCRIPTION_ID_CHANGE));
-        verify(mWifiManager).registerSoftApCallback(any(), any());
         mPhoneStateListener = phoneListenerCaptor.getValue();
+
+        final ArgumentCaptor<SoftApCallback> softApCallbackCaptor =
+                ArgumentCaptor.forClass(SoftApCallback.class);
+        verify(mWifiManager).registerSoftApCallback(any(), softApCallbackCaptor.capture());
+        mSoftApCallback = softApCallbackCaptor.getValue();
     }
 
     private void setTetheringSupported(final boolean supported) {
@@ -849,6 +873,7 @@
         verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
 
         sendIPv6TetherUpdates(upstreamState);
+        verify(mDadProxy, never()).setUpstreamIface(notNull());
         verify(mRouterAdvertisementDaemon, never()).buildNewRa(any(), notNull());
         verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
                 any(), any());
@@ -875,6 +900,8 @@
         verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
 
         sendIPv6TetherUpdates(upstreamState);
+        // TODO: add interfaceParams to compare in verify.
+        verify(mDadProxy, times(1)).setUpstreamIface(notNull());
         verify(mRouterAdvertisementDaemon, times(1)).buildNewRa(any(), notNull());
         verify(mNetd, times(1)).tetherApplyDnsInterfaces();
     }
@@ -891,6 +918,7 @@
                 any(), any());
 
         sendIPv6TetherUpdates(upstreamState);
+        verify(mDadProxy, times(1)).setUpstreamIface(notNull());
         verify(mRouterAdvertisementDaemon, times(1)).buildNewRa(any(), notNull());
         verify(mNetd, times(1)).tetherApplyDnsInterfaces();
     }
@@ -908,6 +936,7 @@
         verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
 
         sendIPv6TetherUpdates(upstreamState);
+        verify(mDadProxy, times(1)).setUpstreamIface(notNull());
         verify(mRouterAdvertisementDaemon, times(1)).buildNewRa(any(), notNull());
         verify(mNetd, times(1)).tetherApplyDnsInterfaces();
     }
@@ -1271,6 +1300,7 @@
                 new ArrayList<>();
         private final ArrayList<TetherStatesParcel> mTetherStates = new ArrayList<>();
         private final ArrayList<Integer> mOffloadStatus = new ArrayList<>();
+        private final ArrayList<List<TetheredClient>> mTetheredClients = new ArrayList<>();
 
         // This function will remove the recorded callbacks, so it must be called once for
         // each callback. If this is called after multiple callback, the order matters.
@@ -1316,6 +1346,13 @@
             return mTetherStates.remove(0);
         }
 
+        public void expectTetheredClientChanged(List<TetheredClient> leases) {
+            assertFalse(mTetheredClients.isEmpty());
+            final List<TetheredClient> result = mTetheredClients.remove(0);
+            assertEquals(leases.size(), result.size());
+            assertTrue(leases.containsAll(result));
+        }
+
         @Override
         public void onUpstreamChanged(Network network) {
             mActualUpstreams.add(network);
@@ -1333,7 +1370,7 @@
 
         @Override
         public void onTetherClientsChanged(List<TetheredClient> clients) {
-            // TODO: check this
+            mTetheredClients.add(clients);
         }
 
         @Override
@@ -1347,6 +1384,7 @@
             mTetheringConfigs.add(parcel.config);
             mTetherStates.add(parcel.states);
             mOffloadStatus.add(parcel.offloadStatus);
+            mTetheredClients.add(parcel.tetheredClients);
         }
 
         @Override
@@ -1376,6 +1414,7 @@
             assertNoUpstreamChangeCallback();
             assertNoConfigChangeCallback();
             assertNoStateChangeCallback();
+            assertTrue(mTetheredClients.isEmpty());
         }
 
         private void assertTetherConfigParcelEqual(@NonNull TetheringConfigurationParcel actual,
@@ -1415,6 +1454,7 @@
         // 1. Register one callback before running any tethering.
         mTethering.registerTetheringEventCallback(callback);
         mLooper.dispatchAll();
+        callback.expectTetheredClientChanged(Collections.emptyList());
         callback.expectUpstreamChanged(new Network[] {null});
         callback.expectConfigurationChanged(
                 mTethering.getTetheringConfiguration().toStableParcelable());
@@ -1441,6 +1481,7 @@
         // 3. Register second callback.
         mTethering.registerTetheringEventCallback(callback2);
         mLooper.dispatchAll();
+        callback2.expectTetheredClientChanged(Collections.emptyList());
         callback2.expectUpstreamChanged(upstreamState.network);
         callback2.expectConfigurationChanged(
                 mTethering.getTetheringConfiguration().toStableParcelable());
@@ -2032,6 +2073,114 @@
         verify(mPackageManager).getPackageInfo(PROVISIONING_APP_NAME[0], GET_ACTIVITIES);
     }
 
+    @Test
+    public void testUpdateConnectedClients() throws Exception {
+        TestTetheringEventCallback callback = new TestTetheringEventCallback();
+        runAsShell(NETWORK_SETTINGS, () -> {
+            mTethering.registerTetheringEventCallback(callback);
+            mLooper.dispatchAll();
+        });
+        callback.expectTetheredClientChanged(Collections.emptyList());
+
+        IDhcpEventCallbacks eventCallbacks;
+        final ArgumentCaptor<IDhcpEventCallbacks> dhcpEventCbsCaptor =
+                 ArgumentCaptor.forClass(IDhcpEventCallbacks.class);
+        // Run local only tethering.
+        mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true);
+        sendWifiP2pConnectionChanged(true, true, TEST_P2P_IFNAME);
+        mLooper.dispatchAll();
+        verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS)).startWithCallbacks(
+                any(), dhcpEventCbsCaptor.capture());
+        eventCallbacks = dhcpEventCbsCaptor.getValue();
+        // Update lease for local only tethering.
+        final MacAddress testMac1 = MacAddress.fromString("11:11:11:11:11:11");
+        final ArrayList<DhcpLeaseParcelable> p2pLeases = new ArrayList<>();
+        p2pLeases.add(createDhcpLeaseParcelable("clientId1", testMac1, "192.168.50.24", 24,
+                Long.MAX_VALUE, "test1"));
+        notifyDhcpLeasesChanged(p2pLeases, eventCallbacks);
+        final List<TetheredClient> clients = toTetheredClients(p2pLeases, TETHERING_WIFI_P2P);
+        callback.expectTetheredClientChanged(clients);
+        reset(mDhcpServer);
+
+        // Run wifi tethering.
+        mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+        sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+        mLooper.dispatchAll();
+        verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS)).startWithCallbacks(
+                any(), dhcpEventCbsCaptor.capture());
+        eventCallbacks = dhcpEventCbsCaptor.getValue();
+        // Update mac address from softAp callback before getting dhcp lease.
+        final ArrayList<WifiClient> wifiClients = new ArrayList<>();
+        final MacAddress testMac2 = MacAddress.fromString("22:22:22:22:22:22");
+        final WifiClient testClient = mock(WifiClient.class);
+        when(testClient.getMacAddress()).thenReturn(testMac2);
+        wifiClients.add(testClient);
+        mSoftApCallback.onConnectedClientsChanged(wifiClients);
+        final TetheredClient noAddrClient = new TetheredClient(testMac2,
+                Collections.emptyList() /* addresses */, TETHERING_WIFI);
+        clients.add(noAddrClient);
+        callback.expectTetheredClientChanged(clients);
+
+        // Update dhcp lease for wifi tethering.
+        clients.remove(noAddrClient);
+        final ArrayList<DhcpLeaseParcelable> wifiLeases = new ArrayList<>();
+        wifiLeases.add(createDhcpLeaseParcelable("clientId2", testMac2, "192.168.43.24", 24,
+                Long.MAX_VALUE, "test2"));
+        notifyDhcpLeasesChanged(wifiLeases, eventCallbacks);
+        clients.addAll(toTetheredClients(wifiLeases, TETHERING_WIFI));
+        callback.expectTetheredClientChanged(clients);
+
+        // Test onStarted callback that register second callback when tethering is running.
+        TestTetheringEventCallback callback2 = new TestTetheringEventCallback();
+        runAsShell(NETWORK_SETTINGS, () -> {
+            mTethering.registerTetheringEventCallback(callback2);
+            mLooper.dispatchAll();
+        });
+        callback2.expectTetheredClientChanged(clients);
+    }
+
+    private void notifyDhcpLeasesChanged(List<DhcpLeaseParcelable> leaseParcelables,
+            IDhcpEventCallbacks callback) throws Exception {
+        callback.onLeasesChanged(leaseParcelables);
+        mLooper.dispatchAll();
+    }
+
+    private List<TetheredClient> toTetheredClients(List<DhcpLeaseParcelable> leaseParcelables,
+            int type) throws Exception {
+        final ArrayList<TetheredClient> leases = new ArrayList<>();
+        for (DhcpLeaseParcelable lease : leaseParcelables) {
+            final LinkAddress address = new LinkAddress(
+                    intToInet4AddressHTH(lease.netAddr), lease.prefixLength,
+                    0 /* flags */, RT_SCOPE_UNIVERSE /* as per RFC6724#3.2 */,
+                    lease.expTime /* deprecationTime */, lease.expTime /* expirationTime */);
+
+            final MacAddress macAddress = MacAddress.fromBytes(lease.hwAddr);
+
+            final AddressInfo addressInfo = new TetheredClient.AddressInfo(address, lease.hostname);
+            leases.add(new TetheredClient(
+                    macAddress,
+                    Collections.singletonList(addressInfo),
+                    type));
+        }
+
+        return leases;
+    }
+
+    private DhcpLeaseParcelable createDhcpLeaseParcelable(final String clientId,
+            final MacAddress hwAddr, final String netAddr, final int prefixLength,
+            final long expTime, final String hostname) throws Exception {
+        final DhcpLeaseParcelable lease = new DhcpLeaseParcelable();
+        lease.clientId = clientId.getBytes();
+        lease.hwAddr = hwAddr.toByteArray();
+        lease.netAddr = inet4AddressToIntHTH(
+                (Inet4Address) InetAddresses.parseNumericAddress(netAddr));
+        lease.prefixLength = prefixLength;
+        lease.expTime = expTime;
+        lease.hostname = hostname;
+
+        return lease;
+    }
+
     // TODO: Test that a request for hotspot mode doesn't interfere with an
     // already operating tethering mode interface.
 }