Merge changes I47b91c0c,I630c0e49 into main

* changes:
  Report sent query count
  Report service info callback metrics data
diff --git a/Cronet/tests/mts/jarjar_excludes.txt b/Cronet/tests/mts/jarjar_excludes.txt
index e8fd39b..a0ce5c2 100644
--- a/Cronet/tests/mts/jarjar_excludes.txt
+++ b/Cronet/tests/mts/jarjar_excludes.txt
@@ -1,3 +1,5 @@
+# Exclude some test prefixes, as they can't be found after being jarjared.
+com\.android\.testutils\..+
 # jarjar-gen can't handle some kotlin object expression, exclude packages that include them
 androidx\..+
 kotlin\.test\..+
diff --git a/OWNERS_core_networking_xts b/OWNERS_core_networking_xts
index 1844334..7612210 100644
--- a/OWNERS_core_networking_xts
+++ b/OWNERS_core_networking_xts
@@ -4,4 +4,6 @@
 # For cherry-picks of CLs that are already merged in aosp/master, or flaky test fixes.
 jchalard@google.com #{LAST_RESORT_SUGGESTION}
 maze@google.com #{LAST_RESORT_SUGGESTION}
+# In addition to cherry-picks and flaky test fixes, also for incremental changes on NsdManager tests
+# to increase coverage for existing behavior, and testing of bug fixes in NsdManager
 reminv@google.com #{LAST_RESORT_SUGGESTION}
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 76e4af8..d33453c 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -140,6 +140,9 @@
     },
     {
       "name": "FrameworksNetTests"
+    },
+    {
+      "name": "NetHttpCoverageTests"
     }
   ],
   "mainline-presubmit": [
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index cf9b359..d3b01ea 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -203,6 +203,8 @@
         // result in a build failure due to inconsistent flags.
         package_prefixes: [
             "android.nearby.aidl",
+            "android.remoteauth.aidl",
+            "android.remoteauth",
             "android.net.apf",
             "android.net.connectivity",
             "android.net.http.apihelpers",
diff --git a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
index 4c9460b..0df9047 100644
--- a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
@@ -17,8 +17,6 @@
 package com.android.networkstack.tethering.apishim.api30;
 
 import android.net.INetd;
-import android.net.IpPrefix;
-import android.net.MacAddress;
 import android.net.TetherStatsParcel;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
@@ -33,7 +31,8 @@
 import com.android.net.module.util.bpf.Tether4Value;
 import com.android.net.module.util.bpf.TetherStatsValue;
 import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
-import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6DownstreamRule;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6UpstreamRule;
 
 /**
  * Bpf coordinator class for API shims.
@@ -58,7 +57,17 @@
     };
 
     @Override
-    public boolean tetherOffloadRuleAdd(@NonNull final Ipv6ForwardingRule rule) {
+    public boolean addIpv6UpstreamRule(@NonNull final Ipv6UpstreamRule rule) {
+        return true;
+    };
+
+    @Override
+    public boolean removeIpv6UpstreamRule(@NonNull final Ipv6UpstreamRule rule) {
+        return true;
+    }
+
+    @Override
+    public boolean addIpv6DownstreamRule(@NonNull final Ipv6DownstreamRule rule) {
         try {
             mNetd.tetherOffloadRuleAdd(rule.toTetherOffloadRuleParcel());
         } catch (RemoteException | ServiceSpecificException e) {
@@ -70,7 +79,7 @@
     };
 
     @Override
-    public boolean tetherOffloadRuleRemove(@NonNull final Ipv6ForwardingRule rule) {
+    public boolean removeIpv6DownstreamRule(@NonNull final Ipv6DownstreamRule rule) {
         try {
             mNetd.tetherOffloadRuleRemove(rule.toTetherOffloadRuleParcel());
         } catch (RemoteException | ServiceSpecificException e) {
@@ -81,19 +90,6 @@
     }
 
     @Override
-    public boolean startUpstreamIpv6Forwarding(int downstreamIfindex, int upstreamIfindex,
-            @NonNull IpPrefix sourcePrefix, @NonNull MacAddress inDstMac,
-            @NonNull MacAddress outSrcMac, @NonNull MacAddress outDstMac, int mtu) {
-        return true;
-    }
-
-    @Override
-    public boolean stopUpstreamIpv6Forwarding(int downstreamIfindex, int upstreamIfindex,
-            @NonNull IpPrefix sourcePrefix, @NonNull MacAddress inDstMac) {
-        return true;
-    }
-
-    @Override
     @Nullable
     public SparseArray<TetherStatsValue> tetherOffloadGetStats() {
         final TetherStatsParcel[] tetherStatsList;
diff --git a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
index 062ecc5..a280046 100644
--- a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
@@ -17,10 +17,9 @@
 package com.android.networkstack.tethering.apishim.api31;
 
 import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED;
-import static android.net.util.NetworkConstants.RFC7421_PREFIX_LENGTH;
 
-import android.net.IpPrefix;
-import android.net.MacAddress;
+import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH;
+
 import android.system.ErrnoException;
 import android.system.Os;
 import android.system.OsConstants;
@@ -38,7 +37,8 @@
 import com.android.net.module.util.bpf.TetherStatsKey;
 import com.android.net.module.util.bpf.TetherStatsValue;
 import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
-import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6DownstreamRule;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6UpstreamRule;
 import com.android.networkstack.tethering.BpfUtils;
 import com.android.networkstack.tethering.Tether6Value;
 import com.android.networkstack.tethering.TetherDevKey;
@@ -50,9 +50,6 @@
 
 import java.io.FileDescriptor;
 import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.util.Arrays;
 
 /**
  * Bpf coordinator class for API shims.
@@ -169,7 +166,40 @@
     }
 
     @Override
-    public boolean tetherOffloadRuleAdd(@NonNull final Ipv6ForwardingRule rule) {
+    public boolean addIpv6UpstreamRule(@NonNull final Ipv6UpstreamRule rule) {
+        if (!isInitialized()) return false;
+        // RFC7421_PREFIX_LENGTH = 64 which is the most commonly used IPv6 subnet prefix length.
+        if (rule.sourcePrefix.getPrefixLength() != RFC7421_PREFIX_LENGTH) return false;
+
+        final TetherUpstream6Key key = rule.makeTetherUpstream6Key();
+        final Tether6Value value = rule.makeTether6Value();
+
+        try {
+            mBpfUpstream6Map.insertEntry(key, value);
+        } catch (ErrnoException | IllegalStateException e) {
+            mLog.e("Could not insert upstream IPv6 entry: " + e);
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public boolean removeIpv6UpstreamRule(@NonNull final Ipv6UpstreamRule rule) {
+        if (!isInitialized()) return false;
+        // RFC7421_PREFIX_LENGTH = 64 which is the most commonly used IPv6 subnet prefix length.
+        if (rule.sourcePrefix.getPrefixLength() != RFC7421_PREFIX_LENGTH) return false;
+
+        try {
+            mBpfUpstream6Map.deleteEntry(rule.makeTetherUpstream6Key());
+        } catch (ErrnoException e) {
+            mLog.e("Could not delete upstream IPv6 entry: " + e);
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public boolean addIpv6DownstreamRule(@NonNull final Ipv6DownstreamRule rule) {
         if (!isInitialized()) return false;
 
         final TetherDownstream6Key key = rule.makeTetherDownstream6Key();
@@ -186,7 +216,7 @@
     }
 
     @Override
-    public boolean tetherOffloadRuleRemove(@NonNull final Ipv6ForwardingRule rule) {
+    public boolean removeIpv6DownstreamRule(@NonNull final Ipv6DownstreamRule rule) {
         if (!isInitialized()) return false;
 
         try {
@@ -201,51 +231,6 @@
         return true;
     }
 
-    @NonNull
-    private TetherUpstream6Key makeUpstream6Key(int downstreamIfindex, @NonNull MacAddress inDstMac,
-            @NonNull IpPrefix sourcePrefix) {
-        byte[] prefixBytes = Arrays.copyOf(sourcePrefix.getRawAddress(), 8);
-        long prefix64 = ByteBuffer.wrap(prefixBytes).order(ByteOrder.BIG_ENDIAN).getLong();
-        return new TetherUpstream6Key(downstreamIfindex, inDstMac, prefix64);
-    }
-
-    @Override
-    public boolean startUpstreamIpv6Forwarding(int downstreamIfindex, int upstreamIfindex,
-            @NonNull IpPrefix sourcePrefix, @NonNull MacAddress inDstMac,
-            @NonNull MacAddress outSrcMac, @NonNull MacAddress outDstMac, int mtu) {
-        if (!isInitialized()) return false;
-        // RFC7421_PREFIX_LENGTH = 64 which is the most commonly used IPv6 subnet prefix length.
-        if (sourcePrefix.getPrefixLength() != RFC7421_PREFIX_LENGTH) return false;
-
-        final TetherUpstream6Key key = makeUpstream6Key(downstreamIfindex, inDstMac, sourcePrefix);
-        final Tether6Value value = new Tether6Value(upstreamIfindex, outSrcMac,
-                outDstMac, OsConstants.ETH_P_IPV6, mtu);
-        try {
-            mBpfUpstream6Map.insertEntry(key, value);
-        } catch (ErrnoException | IllegalStateException e) {
-            mLog.e("Could not insert upstream6 entry: " + e);
-            return false;
-        }
-        return true;
-    }
-
-    @Override
-    public boolean stopUpstreamIpv6Forwarding(int downstreamIfindex, int upstreamIfindex,
-            @NonNull IpPrefix sourcePrefix, @NonNull MacAddress inDstMac) {
-        if (!isInitialized()) return false;
-        // RFC7421_PREFIX_LENGTH = 64 which is the most commonly used IPv6 subnet prefix length.
-        if (sourcePrefix.getPrefixLength() != RFC7421_PREFIX_LENGTH) return false;
-
-        final TetherUpstream6Key key = makeUpstream6Key(downstreamIfindex, inDstMac, sourcePrefix);
-        try {
-            mBpfUpstream6Map.deleteEntry(key);
-        } catch (ErrnoException e) {
-            mLog.e("Could not delete upstream IPv6 entry: " + e);
-            return false;
-        }
-        return true;
-    }
-
     @Override
     @Nullable
     public SparseArray<TetherStatsValue> tetherOffloadGetStats() {
diff --git a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
index 25fa8bc..d28a397 100644
--- a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
+++ b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
@@ -16,8 +16,6 @@
 
 package com.android.networkstack.tethering.apishim.common;
 
-import android.net.IpPrefix;
-import android.net.MacAddress;
 import android.util.SparseArray;
 
 import androidx.annotation.NonNull;
@@ -28,7 +26,8 @@
 import com.android.net.module.util.bpf.Tether4Value;
 import com.android.net.module.util.bpf.TetherStatsValue;
 import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
-import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6DownstreamRule;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6UpstreamRule;
 
 /**
  * Bpf coordinator class for API shims.
@@ -54,53 +53,51 @@
     public abstract boolean isInitialized();
 
     /**
-     * Adds a tethering offload rule to BPF map, or updates it if it already exists.
+     * Adds a tethering offload upstream rule to BPF map, or updates it if it already exists.
+     *
+     * An existing rule will be updated if the input interface, destination MAC and source prefix
+     * match. Otherwise, a new rule will be created. Note that this can be only called on handler
+     * thread.
+     *
+     * @param rule The rule to add or update.
+     * @return true if operation succeeded or was a no-op, false otherwise.
+     */
+    public abstract boolean addIpv6UpstreamRule(@NonNull Ipv6UpstreamRule rule);
+
+    /**
+     * Deletes a tethering offload upstream rule from the BPF map.
+     *
+     * An existing rule will be deleted if the input interface, destination MAC and source prefix
+     * match. It is not an error if there is no matching rule to delete.
+     *
+     * @param rule The rule to delete.
+     * @return true if operation succeeded or was a no-op, false otherwise.
+     */
+    public abstract boolean removeIpv6UpstreamRule(@NonNull Ipv6UpstreamRule rule);
+
+    /**
+     * Adds a tethering offload downstream rule to BPF map, or updates it if it already exists.
      *
      * Currently, only downstream /128 IPv6 entries are supported. An existing rule will be updated
      * if the input interface and destination prefix match. Otherwise, a new rule will be created.
      * Note that this can be only called on handler thread.
      *
      * @param rule The rule to add or update.
+     * @return true if operation succeeded or was a no-op, false otherwise.
      */
-    public abstract boolean tetherOffloadRuleAdd(@NonNull Ipv6ForwardingRule rule);
+    public abstract boolean addIpv6DownstreamRule(@NonNull Ipv6DownstreamRule rule);
 
     /**
-     * Deletes a tethering offload rule from the BPF map.
+     * Deletes a tethering offload downstream rule from the BPF map.
      *
      * Currently, only downstream /128 IPv6 entries are supported. An existing rule will be deleted
      * if the destination IP address and the source interface match. It is not an error if there is
      * no matching rule to delete.
      *
      * @param rule The rule to delete.
+     * @return true if operation succeeded or was a no-op, false otherwise.
      */
-    public abstract boolean tetherOffloadRuleRemove(@NonNull Ipv6ForwardingRule rule);
-
-    /**
-     * Starts IPv6 forwarding between the specified interfaces.
-
-     * @param downstreamIfindex the downstream interface index
-     * @param upstreamIfindex the upstream interface index
-     * @param sourcePrefix the source IPv6 prefix
-     * @param inDstMac the destination MAC address to use for XDP
-     * @param outSrcMac the source MAC address to use for packets
-     * @param outDstMac the destination MAC address to use for packets
-     * @return true if operation succeeded or was a no-op, false otherwise
-     */
-    public abstract boolean startUpstreamIpv6Forwarding(int downstreamIfindex, int upstreamIfindex,
-            @NonNull IpPrefix sourcePrefix, @NonNull MacAddress inDstMac,
-            @NonNull MacAddress outSrcMac, @NonNull MacAddress outDstMac, int mtu);
-
-    /**
-     * Stops IPv6 forwarding between the specified interfaces.
-
-     * @param downstreamIfindex the downstream interface index
-     * @param upstreamIfindex the upstream interface index
-     * @param sourcePrefix the valid source IPv6 prefix
-     * @param inDstMac the destination MAC address to use for XDP
-     * @return true if operation succeeded or was a no-op, false otherwise
-     */
-    public abstract boolean stopUpstreamIpv6Forwarding(int downstreamIfindex, int upstreamIfindex,
-            @NonNull IpPrefix sourcePrefix, @NonNull MacAddress inDstMac);
+    public abstract boolean removeIpv6DownstreamRule(@NonNull Ipv6DownstreamRule rule);
 
     /**
      * Return BPF tethering offload statistics.
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index 6affb62..bb09d0d 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -28,11 +28,11 @@
 import static android.net.TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
 import static android.net.TetheringManager.TetheringRequest.checkStaticAddressConfiguration;
 import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS;
-import static android.net.util.NetworkConstants.RFC7421_PREFIX_LENGTH;
 import static android.net.util.NetworkConstants.asByte;
 import static android.system.OsConstants.RT_SCOPE_UNIVERSE;
 
 import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
+import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH;
 import static com.android.networkstack.tethering.UpstreamNetworkState.isVcnInterface;
 import static com.android.networkstack.tethering.util.PrefixUtils.asIpPrefix;
 import static com.android.networkstack.tethering.util.TetheringMessageBase.BASE_IPSERVER;
@@ -77,7 +77,7 @@
 import com.android.net.module.util.ip.IpNeighborMonitor.NeighborEvent;
 import com.android.networkstack.tethering.BpfCoordinator;
 import com.android.networkstack.tethering.BpfCoordinator.ClientInfo;
-import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6DownstreamRule;
 import com.android.networkstack.tethering.PrivateAddressCoordinator;
 import com.android.networkstack.tethering.TetheringConfiguration;
 import com.android.networkstack.tethering.metrics.TetheringMetrics;
@@ -327,8 +327,8 @@
 
         // IP neighbor monitor monitors the neighbor events for adding/removing offload
         // forwarding rules per client. If BPF offload is not supported, don't start listening
-        // for neighbor events. See updateIpv6ForwardingRules, addIpv6ForwardingRule,
-        // removeIpv6ForwardingRule.
+        // for neighbor events. See updateIpv6ForwardingRules, addIpv6DownstreamRule,
+        // removeIpv6DownstreamRule.
         if (mUsingBpfOffload && !mIpNeighborMonitor.start()) {
             mLog.e("Failed to create IpNeighborMonitor on " + mIfaceName);
         }
@@ -890,21 +890,21 @@
         }
     }
 
-    private void addIpv6ForwardingRule(Ipv6ForwardingRule rule) {
+    private void addIpv6DownstreamRule(Ipv6DownstreamRule rule) {
         // Theoretically, we don't need this check because IP neighbor monitor doesn't start if BPF
         // offload is disabled. Add this check just in case.
         // TODO: Perhaps remove this protection check.
         if (!mUsingBpfOffload) return;
 
-        mBpfCoordinator.tetherOffloadRuleAdd(this, rule);
+        mBpfCoordinator.addIpv6DownstreamRule(this, rule);
     }
 
-    private void removeIpv6ForwardingRule(Ipv6ForwardingRule rule) {
+    private void removeIpv6DownstreamRule(Ipv6DownstreamRule rule) {
         // TODO: Perhaps remove this protection check.
-        // See the related comment in #addIpv6ForwardingRule.
+        // See the related comment in #addIpv6DownstreamRule.
         if (!mUsingBpfOffload) return;
 
-        mBpfCoordinator.tetherOffloadRuleRemove(this, rule);
+        mBpfCoordinator.removeIpv6DownstreamRule(this, rule);
     }
 
     private void clearIpv6ForwardingRules() {
@@ -915,7 +915,7 @@
 
     private void updateIpv6ForwardingRule(int newIfindex) {
         // TODO: Perhaps remove this protection check.
-        // See the related comment in #addIpv6ForwardingRule.
+        // See the related comment in #addIpv6DownstreamRule.
         if (!mUsingBpfOffload) return;
 
         mBpfCoordinator.tetherOffloadRuleUpdate(this, newIfindex);
@@ -954,22 +954,22 @@
         }
 
         // When deleting rules, we still need to pass a non-null MAC, even though it's ignored.
-        // Do this here instead of in the Ipv6ForwardingRule constructor to ensure that we never
-        // add rules with a null MAC, only delete them.
+        // Do this here instead of in the Ipv6DownstreamRule constructor to ensure that we
+        // never add rules with a null MAC, only delete them.
         MacAddress dstMac = e.isValid() ? e.macAddr : NULL_MAC_ADDRESS;
-        Ipv6ForwardingRule rule = new Ipv6ForwardingRule(upstreamIfindex,
-                mInterfaceParams.index, (Inet6Address) e.ip, mInterfaceParams.macAddr, dstMac);
+        Ipv6DownstreamRule rule = new Ipv6DownstreamRule(upstreamIfindex, mInterfaceParams.index,
+                (Inet6Address) e.ip, mInterfaceParams.macAddr, dstMac);
         if (e.isValid()) {
-            addIpv6ForwardingRule(rule);
+            addIpv6DownstreamRule(rule);
         } else {
-            removeIpv6ForwardingRule(rule);
+            removeIpv6DownstreamRule(rule);
         }
     }
 
     // TODO: consider moving into BpfCoordinator.
     private void updateClientInfoIpv4(NeighborEvent e) {
         // TODO: Perhaps remove this protection check.
-        // See the related comment in #addIpv6ForwardingRule.
+        // See the related comment in #addIpv6DownstreamRule.
         if (!mUsingBpfOffload) return;
 
         if (e == null) return;
diff --git a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
index 18c2171..50d6c4b 100644
--- a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
+++ b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
@@ -16,7 +16,6 @@
 
 package android.net.ip;
 
-import static android.net.util.NetworkConstants.RFC7421_PREFIX_LENGTH;
 import static android.system.OsConstants.AF_INET6;
 import static android.system.OsConstants.IPPROTO_ICMPV6;
 import static android.system.OsConstants.SOCK_RAW;
@@ -30,6 +29,7 @@
 import static com.android.net.module.util.NetworkStackConstants.IPV6_MIN_MTU;
 import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_AUTONOMOUS;
 import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_ON_LINK;
+import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH;
 import static com.android.net.module.util.NetworkStackConstants.TAG_SYSTEM_NEIGHBOR;
 import static com.android.networkstack.tethering.util.TetheringUtils.getAllNodesForScopeId;
 
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index f22ccbd..7311125 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -88,6 +88,8 @@
 import java.net.NetworkInterface;
 import java.net.SocketException;
 import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -235,8 +237,8 @@
     // rules function without a valid IPv6 downstream interface index even if it may have one
     // before. IpServer would need to call getInterfaceParams() in the constructor instead of when
     // startIpv6() is called, and make mInterfaceParams final.
-    private final HashMap<IpServer, LinkedHashMap<Inet6Address, Ipv6ForwardingRule>>
-            mIpv6ForwardingRules = new LinkedHashMap<>();
+    private final HashMap<IpServer, LinkedHashMap<Inet6Address, Ipv6DownstreamRule>>
+            mIpv6DownstreamRules = new LinkedHashMap<>();
 
     // Map of downstream client maps. Each of these maps represents the IPv4 clients for a given
     // downstream. Needed to build IPv4 forwarding rules when conntrack events are received.
@@ -499,8 +501,8 @@
     /**
      * Stop BPF tethering offload stats polling.
      * The data limit cleanup and the tether stats maps cleanup are not implemented here.
-     * These cleanups rely on all IpServers calling #tetherOffloadRuleRemove. After the
-     * last rule is removed from the upstream, #tetherOffloadRuleRemove does the cleanup
+     * These cleanups rely on all IpServers calling #removeIpv6DownstreamRule. After the
+     * last rule is removed from the upstream, #removeIpv6DownstreamRule does the cleanup
      * functionality.
      * Note that this can be only called on handler thread.
      */
@@ -589,22 +591,22 @@
     }
 
     /**
-     * Add forwarding rule. After adding the first rule on a given upstream, must add the data
+     * Add IPv6 downstream rule. After adding the first rule on a given upstream, must add the data
      * limit on the given upstream.
      * Note that this can be only called on handler thread.
      */
-    public void tetherOffloadRuleAdd(
-            @NonNull final IpServer ipServer, @NonNull final Ipv6ForwardingRule rule) {
+    public void addIpv6DownstreamRule(
+            @NonNull final IpServer ipServer, @NonNull final Ipv6DownstreamRule rule) {
         if (!isUsingBpf()) return;
 
         // TODO: Perhaps avoid to add a duplicate rule.
-        if (!mBpfCoordinatorShim.tetherOffloadRuleAdd(rule)) return;
+        if (!mBpfCoordinatorShim.addIpv6DownstreamRule(rule)) return;
 
-        if (!mIpv6ForwardingRules.containsKey(ipServer)) {
-            mIpv6ForwardingRules.put(ipServer, new LinkedHashMap<Inet6Address,
-                    Ipv6ForwardingRule>());
+        if (!mIpv6DownstreamRules.containsKey(ipServer)) {
+            mIpv6DownstreamRules.put(ipServer, new LinkedHashMap<Inet6Address,
+                    Ipv6DownstreamRule>());
         }
-        LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules = mIpv6ForwardingRules.get(ipServer);
+        LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules = mIpv6DownstreamRules.get(ipServer);
 
         // Add upstream and downstream interface index to dev map.
         maybeAddDevMap(rule.upstreamIfindex, rule.downstreamIfindex);
@@ -613,15 +615,13 @@
         maybeSetLimit(rule.upstreamIfindex);
 
         if (!isAnyRuleFromDownstreamToUpstream(rule.downstreamIfindex, rule.upstreamIfindex)) {
-            final int downstream = rule.downstreamIfindex;
-            final int upstream = rule.upstreamIfindex;
             // TODO: support upstream forwarding on non-point-to-point interfaces.
             // TODO: get the MTU from LinkProperties and update the rules when it changes.
-            if (!mBpfCoordinatorShim.startUpstreamIpv6Forwarding(downstream, upstream,
-                    IPV6_ZERO_PREFIX64, rule.srcMac, NULL_MAC_ADDRESS, NULL_MAC_ADDRESS,
-                    NetworkStackConstants.ETHER_MTU)) {
-                mLog.e("Failed to enable upstream IPv6 forwarding from "
-                        + getIfName(downstream) + " to " + getIfName(upstream));
+            Ipv6UpstreamRule upstreamRule = new Ipv6UpstreamRule(rule.upstreamIfindex,
+                    rule.downstreamIfindex, IPV6_ZERO_PREFIX64, rule.srcMac, NULL_MAC_ADDRESS,
+                    NULL_MAC_ADDRESS);
+            if (!mBpfCoordinatorShim.addIpv6UpstreamRule(upstreamRule)) {
+                mLog.e("Failed to add upstream IPv6 forwarding rule: " + upstreamRule);
             }
         }
 
@@ -631,17 +631,17 @@
     }
 
     /**
-     * Remove forwarding rule. After removing the last rule on a given upstream, must clear
+     * Remove IPv6 downstream rule. After removing the last rule on a given upstream, must clear
      * data limit, update the last tether stats and remove the tether stats in the BPF maps.
      * Note that this can be only called on handler thread.
      */
-    public void tetherOffloadRuleRemove(
-            @NonNull final IpServer ipServer, @NonNull final Ipv6ForwardingRule rule) {
+    public void removeIpv6DownstreamRule(
+            @NonNull final IpServer ipServer, @NonNull final Ipv6DownstreamRule rule) {
         if (!isUsingBpf()) return;
 
-        if (!mBpfCoordinatorShim.tetherOffloadRuleRemove(rule)) return;
+        if (!mBpfCoordinatorShim.removeIpv6DownstreamRule(rule)) return;
 
-        LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules = mIpv6ForwardingRules.get(ipServer);
+        LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules = mIpv6DownstreamRules.get(ipServer);
         if (rules == null) return;
 
         // Must remove rules before calling #isAnyRuleOnUpstream because it needs to check if
@@ -652,17 +652,16 @@
 
         // Remove the downstream entry if it has no more rule.
         if (rules.isEmpty()) {
-            mIpv6ForwardingRules.remove(ipServer);
+            mIpv6DownstreamRules.remove(ipServer);
         }
 
         // If no more rules between this upstream and downstream, stop upstream forwarding.
         if (!isAnyRuleFromDownstreamToUpstream(rule.downstreamIfindex, rule.upstreamIfindex)) {
-            final int downstream = rule.downstreamIfindex;
-            final int upstream = rule.upstreamIfindex;
-            if (!mBpfCoordinatorShim.stopUpstreamIpv6Forwarding(downstream, upstream,
-                    IPV6_ZERO_PREFIX64, rule.srcMac)) {
-                mLog.e("Failed to disable upstream IPv6 forwarding from "
-                        + getIfName(downstream) + " to " + getIfName(upstream));
+            Ipv6UpstreamRule upstreamRule = new Ipv6UpstreamRule(rule.upstreamIfindex,
+                    rule.downstreamIfindex, IPV6_ZERO_PREFIX64, rule.srcMac, NULL_MAC_ADDRESS,
+                    NULL_MAC_ADDRESS);
+            if (!mBpfCoordinatorShim.removeIpv6UpstreamRule(upstreamRule)) {
+                mLog.e("Failed to remove upstream IPv6 forwarding rule: " + upstreamRule);
             }
         }
 
@@ -678,13 +677,13 @@
     public void tetherOffloadRuleClear(@NonNull final IpServer ipServer) {
         if (!isUsingBpf()) return;
 
-        final LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules = mIpv6ForwardingRules.get(
-                ipServer);
+        final LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules =
+                mIpv6DownstreamRules.get(ipServer);
         if (rules == null) return;
 
         // Need to build a rule list because the rule map may be changed in the iteration.
-        for (final Ipv6ForwardingRule rule : new ArrayList<Ipv6ForwardingRule>(rules.values())) {
-            tetherOffloadRuleRemove(ipServer, rule);
+        for (final Ipv6DownstreamRule rule : new ArrayList<Ipv6DownstreamRule>(rules.values())) {
+            removeIpv6DownstreamRule(ipServer, rule);
         }
     }
 
@@ -695,28 +694,28 @@
     public void tetherOffloadRuleUpdate(@NonNull final IpServer ipServer, int newUpstreamIfindex) {
         if (!isUsingBpf()) return;
 
-        final LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules = mIpv6ForwardingRules.get(
-                ipServer);
+        final LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules =
+                mIpv6DownstreamRules.get(ipServer);
         if (rules == null) return;
 
         // Need to build a rule list because the rule map may be changed in the iteration.
         // First remove all the old rules, then add all the new rules. This is because the upstream
-        // forwarding code in tetherOffloadRuleAdd cannot support rules on two upstreams at the
+        // forwarding code in addIpv6DownstreamRule cannot support rules on two upstreams at the
         // same time. Deleting the rules first ensures that upstream forwarding is disabled on the
         // old upstream when the last rule is removed from it, and re-enabled on the new upstream
         // when the first rule is added to it.
         // TODO: Once the IPv6 client processing code has moved from IpServer to BpfCoordinator, do
         // something smarter.
-        final ArrayList<Ipv6ForwardingRule> rulesCopy = new ArrayList<>(rules.values());
-        for (final Ipv6ForwardingRule rule : rulesCopy) {
+        final ArrayList<Ipv6DownstreamRule> rulesCopy = new ArrayList<>(rules.values());
+        for (final Ipv6DownstreamRule rule : rulesCopy) {
             // Remove the old rule before adding the new one because the map uses the same key for
             // both rules. Reversing the processing order causes that the new rule is removed as
             // unexpected.
             // TODO: Add new rule first to reduce the latency which has no rule.
-            tetherOffloadRuleRemove(ipServer, rule);
+            removeIpv6DownstreamRule(ipServer, rule);
         }
-        for (final Ipv6ForwardingRule rule : rulesCopy) {
-            tetherOffloadRuleAdd(ipServer, rule.onNewUpstream(newUpstreamIfindex));
+        for (final Ipv6DownstreamRule rule : rulesCopy) {
+            addIpv6DownstreamRule(ipServer, rule.onNewUpstream(newUpstreamIfindex));
         }
     }
 
@@ -1142,14 +1141,14 @@
     private void dumpIpv6ForwardingRulesByDownstream(@NonNull IndentingPrintWriter pw) {
         pw.println("IPv6 Forwarding rules by downstream interface:");
         pw.increaseIndent();
-        if (mIpv6ForwardingRules.size() == 0) {
-            pw.println("No IPv6 rules");
+        if (mIpv6DownstreamRules.size() == 0) {
+            pw.println("No downstream IPv6 rules");
             pw.decreaseIndent();
             return;
         }
 
-        for (Map.Entry<IpServer, LinkedHashMap<Inet6Address, Ipv6ForwardingRule>> entry :
-                mIpv6ForwardingRules.entrySet()) {
+        for (Map.Entry<IpServer, LinkedHashMap<Inet6Address, Ipv6DownstreamRule>> entry :
+                mIpv6DownstreamRules.entrySet()) {
             IpServer ipServer = entry.getKey();
             // The rule downstream interface index is paired with the interface name from
             // IpServer#interfaceName. See #startIPv6, #updateIpv6ForwardingRules in IpServer.
@@ -1158,8 +1157,8 @@
                     + "[srcmac] [dstmac]");
 
             pw.increaseIndent();
-            LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules = entry.getValue();
-            for (Ipv6ForwardingRule rule : rules.values()) {
+            LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules = entry.getValue();
+            for (Ipv6DownstreamRule rule : rules.values()) {
                 final int upstreamIfindex = rule.upstreamIfindex;
                 pw.println(String.format("%d(%s) %d(%s) %s [%s] [%s]", upstreamIfindex,
                         getIfName(upstreamIfindex), rule.downstreamIfindex,
@@ -1406,13 +1405,13 @@
         pw.decreaseIndent();
     }
 
-    /** IPv6 forwarding rule class. */
-    public static class Ipv6ForwardingRule {
-        // The upstream6 and downstream6 rules are built as the following tables. Only raw ip
-        // upstream interface is supported.
+    /** IPv6 upstream forwarding rule class. */
+    public static class Ipv6UpstreamRule {
+        // The upstream6 rules are built as the following tables. Only raw ip upstream interface is
+        // supported.
         // TODO: support ether ip upstream interface.
         //
-        // NAT network topology:
+        // Tethering network topology:
         //
         //         public network (rawip)                 private network
         //                   |                 UE                |
@@ -1422,15 +1421,15 @@
         //
         // upstream6 key and value:
         //
-        // +------+-------------+
-        // | TetherUpstream6Key |
-        // +------+------+------+
-        // |field |iif   |dstMac|
-        // |      |      |      |
-        // +------+------+------+
-        // |value |downst|downst|
-        // |      |ream  |ream  |
-        // +------+------+------+
+        // +------+-------------------+
+        // | TetherUpstream6Key       |
+        // +------+------+------+-----+
+        // |field |iif   |dstMac|src64|
+        // |      |      |      |     |
+        // +------+------+------+-----+
+        // |value |downst|downst|upstr|
+        // |      |ream  |ream  |eam  |
+        // +------+------+------+-----+
         //
         // +------+----------------------------------+
         // |      |Tether6Value                      |
@@ -1442,6 +1441,92 @@
         // |      |am    |      |      |IP    |      |
         // +------+------+------+------+------+------+
         //
+        public final int upstreamIfindex;
+        public final int downstreamIfindex;
+        @NonNull
+        public final IpPrefix sourcePrefix;
+        @NonNull
+        public final MacAddress inDstMac;
+        @NonNull
+        public final MacAddress outSrcMac;
+        @NonNull
+        public final MacAddress outDstMac;
+
+        public Ipv6UpstreamRule(int upstreamIfindex, int downstreamIfindex,
+                @NonNull IpPrefix sourcePrefix, @NonNull MacAddress inDstMac,
+                @NonNull MacAddress outSrcMac, @NonNull MacAddress outDstMac) {
+            this.upstreamIfindex = upstreamIfindex;
+            this.downstreamIfindex = downstreamIfindex;
+            this.sourcePrefix = sourcePrefix;
+            this.inDstMac = inDstMac;
+            this.outSrcMac = outSrcMac;
+            this.outDstMac = outDstMac;
+        }
+
+        /**
+         * Return a TetherUpstream6Key object built from the rule.
+         */
+        @NonNull
+        public TetherUpstream6Key makeTetherUpstream6Key() {
+            byte[] prefixBytes = Arrays.copyOf(sourcePrefix.getRawAddress(), 8);
+            long prefix64 = ByteBuffer.wrap(prefixBytes).order(ByteOrder.BIG_ENDIAN).getLong();
+            return new TetherUpstream6Key(downstreamIfindex, inDstMac, prefix64);
+        }
+
+        /**
+         * Return a Tether6Value object built from the rule.
+         */
+        @NonNull
+        public Tether6Value makeTether6Value() {
+            return new Tether6Value(upstreamIfindex, outDstMac, outSrcMac, ETH_P_IPV6,
+                    NetworkStackConstants.ETHER_MTU);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof Ipv6UpstreamRule)) return false;
+            Ipv6UpstreamRule that = (Ipv6UpstreamRule) o;
+            return this.upstreamIfindex == that.upstreamIfindex
+                    && this.downstreamIfindex == that.downstreamIfindex
+                    && Objects.equals(this.sourcePrefix, that.sourcePrefix)
+                    && Objects.equals(this.inDstMac, that.inDstMac)
+                    && Objects.equals(this.outSrcMac, that.outSrcMac)
+                    && Objects.equals(this.outDstMac, that.outDstMac);
+        }
+
+        @Override
+        public int hashCode() {
+            // TODO: if this is ever used in production code, don't pass ifindices
+            // to Objects.hash() to avoid autoboxing overhead.
+            return Objects.hash(upstreamIfindex, downstreamIfindex, sourcePrefix, inDstMac,
+                    outSrcMac, outDstMac);
+        }
+
+        @Override
+        public String toString() {
+            return "upstreamIfindex: " + upstreamIfindex
+                    + ", downstreamIfindex: " + downstreamIfindex
+                    + ", sourcePrefix: " + sourcePrefix
+                    + ", inDstMac: " + inDstMac
+                    + ", outSrcMac: " + outSrcMac
+                    + ", outDstMac: " + outDstMac;
+        }
+    }
+
+    /** IPv6 downstream forwarding rule class. */
+    public static class Ipv6DownstreamRule {
+        // The downstream6 rules are built as the following tables. Only raw ip upstream interface
+        // is supported.
+        // TODO: support ether ip upstream interface.
+        //
+        // Tethering network topology:
+        //
+        //         public network (rawip)                 private network
+        //                   |                 UE                |
+        // +------------+    V    +------------+------------+    V    +------------+
+        // |   Sever    +---------+  Upstream  | Downstream +---------+   Client   |
+        // +------------+         +------------+------------+         +------------+
+        //
         // downstream6 key and value:
         //
         // +------+--------------------+
@@ -1475,11 +1560,11 @@
         @NonNull
         public final MacAddress dstMac;
 
-        public Ipv6ForwardingRule(int upstreamIfindex, int downstreamIfIndex,
+        public Ipv6DownstreamRule(int upstreamIfindex, int downstreamIfindex,
                 @NonNull Inet6Address address, @NonNull MacAddress srcMac,
                 @NonNull MacAddress dstMac) {
             this.upstreamIfindex = upstreamIfindex;
-            this.downstreamIfindex = downstreamIfIndex;
+            this.downstreamIfindex = downstreamIfindex;
             this.address = address;
             this.srcMac = srcMac;
             this.dstMac = dstMac;
@@ -1487,8 +1572,8 @@
 
         /** Return a new rule object which updates with new upstream index. */
         @NonNull
-        public Ipv6ForwardingRule onNewUpstream(int newUpstreamIfindex) {
-            return new Ipv6ForwardingRule(newUpstreamIfindex, downstreamIfindex, address, srcMac,
+        public Ipv6DownstreamRule onNewUpstream(int newUpstreamIfindex) {
+            return new Ipv6DownstreamRule(newUpstreamIfindex, downstreamIfindex, address, srcMac,
                     dstMac);
         }
 
@@ -1528,8 +1613,8 @@
 
         @Override
         public boolean equals(Object o) {
-            if (!(o instanceof Ipv6ForwardingRule)) return false;
-            Ipv6ForwardingRule that = (Ipv6ForwardingRule) o;
+            if (!(o instanceof Ipv6DownstreamRule)) return false;
+            Ipv6DownstreamRule that = (Ipv6DownstreamRule) o;
             return this.upstreamIfindex == that.upstreamIfindex
                     && this.downstreamIfindex == that.downstreamIfindex
                     && Objects.equals(this.address, that.address)
@@ -1870,9 +1955,9 @@
     }
 
     private int getInterfaceIndexFromRules(@NonNull String ifName) {
-        for (LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules : mIpv6ForwardingRules
-                .values()) {
-            for (Ipv6ForwardingRule rule : rules.values()) {
+        for (LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules :
+                mIpv6DownstreamRules.values()) {
+            for (Ipv6DownstreamRule rule : rules.values()) {
                 final int upstreamIfindex = rule.upstreamIfindex;
                 if (TextUtils.equals(ifName, mInterfaceNames.get(upstreamIfindex))) {
                     return upstreamIfindex;
@@ -1963,9 +2048,9 @@
     // TODO: Rename to isAnyIpv6RuleOnUpstream and define an isAnyRuleOnUpstream method that called
     // both isAnyIpv6RuleOnUpstream and mBpfCoordinatorShim.isAnyIpv4RuleOnUpstream.
     private boolean isAnyRuleOnUpstream(int upstreamIfindex) {
-        for (LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules : mIpv6ForwardingRules
-                .values()) {
-            for (Ipv6ForwardingRule rule : rules.values()) {
+        for (LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules :
+                mIpv6DownstreamRules.values()) {
+            for (Ipv6DownstreamRule rule : rules.values()) {
                 if (upstreamIfindex == rule.upstreamIfindex) return true;
             }
         }
@@ -1973,9 +2058,9 @@
     }
 
     private boolean isAnyRuleFromDownstreamToUpstream(int downstreamIfindex, int upstreamIfindex) {
-        for (LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules : mIpv6ForwardingRules
-                .values()) {
-            for (Ipv6ForwardingRule rule : rules.values()) {
+        for (LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules :
+                mIpv6DownstreamRules.values()) {
+            for (Ipv6DownstreamRule rule : rules.values()) {
                 if (downstreamIfindex == rule.downstreamIfindex
                         && upstreamIfindex == rule.upstreamIfindex) {
                     return true;
@@ -2226,13 +2311,13 @@
                 CONNTRACK_TIMEOUT_UPDATE_INTERVAL_MS);
     }
 
-    // Return forwarding rule map. This is used for testing only.
+    // Return IPv6 downstream forwarding rule map. This is used for testing only.
     // Note that this can be only called on handler thread.
     @NonNull
     @VisibleForTesting
-    final HashMap<IpServer, LinkedHashMap<Inet6Address, Ipv6ForwardingRule>>
-            getForwardingRulesForTesting() {
-        return mIpv6ForwardingRules;
+    final HashMap<IpServer, LinkedHashMap<Inet6Address, Ipv6DownstreamRule>>
+            getIpv6DownstreamRulesForTesting() {
+        return mIpv6DownstreamRules;
     }
 
     // Return upstream interface name map. This is used for testing only.
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
index b0aa668..fe70820 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
@@ -112,8 +112,8 @@
      * config_tether_upstream_automatic when set to true.
      *
      * This flag is enabled if !=0 and less than the module APEX version: see
-     * {@link DeviceConfigUtils#isFeatureEnabled}. It is also ignored after R, as later devices
-     * should just set config_tether_upstream_automatic to true instead.
+     * {@link DeviceConfigUtils#isTetheringFeatureEnabled}. It is also ignored after R, as later
+     * devices should just set config_tether_upstream_automatic to true instead.
      */
     public static final String TETHER_FORCE_UPSTREAM_AUTOMATIC_VERSION =
             "tether_force_upstream_automatic_version";
@@ -181,7 +181,7 @@
     public static class Dependencies {
         boolean isFeatureEnabled(@NonNull Context context, @NonNull String namespace,
                 @NonNull String name, @NonNull String moduleName, boolean defaultEnabled) {
-            return DeviceConfigUtils.isFeatureEnabled(context, namespace, name,
+            return DeviceConfigUtils.isTetheringFeatureEnabled(context, namespace, name,
                     moduleName, defaultEnabled);
         }
 
diff --git a/Tethering/tests/integration/Android.bp b/Tethering/tests/integration/Android.bp
index 5e08aba..20f0bc6 100644
--- a/Tethering/tests/integration/Android.bp
+++ b/Tethering/tests/integration/Android.bp
@@ -25,11 +25,12 @@
     ],
     min_sdk_version: "30",
     static_libs: [
-        "NetworkStackApiStableLib",
+        "DhcpPacketLib",
         "androidx.test.rules",
         "cts-net-utils",
         "mockito-target-extended-minus-junit4",
         "net-tests-utils",
+        "net-utils-device-common",
         "net-utils-device-common-bpf",
         "testables",
         "connectivity-net-module-utils-bpf",
diff --git a/Tethering/tests/integration/base/android/net/TetheringTester.java b/Tethering/tests/integration/base/android/net/TetheringTester.java
index 3f3768e..4f3c6e7 100644
--- a/Tethering/tests/integration/base/android/net/TetheringTester.java
+++ b/Tethering/tests/integration/base/android/net/TetheringTester.java
@@ -74,6 +74,7 @@
 import com.android.net.module.util.Ipv6Utils;
 import com.android.net.module.util.PacketBuilder;
 import com.android.net.module.util.Struct;
+import com.android.net.module.util.arp.ArpPacket;
 import com.android.net.module.util.structs.EthernetHeader;
 import com.android.net.module.util.structs.Icmpv4Header;
 import com.android.net.module.util.structs.Icmpv6Header;
@@ -85,7 +86,6 @@
 import com.android.net.module.util.structs.RaHeader;
 import com.android.net.module.util.structs.TcpHeader;
 import com.android.net.module.util.structs.UdpHeader;
-import com.android.networkstack.arp.ArpPacket;
 import com.android.testutils.TapPacketReader;
 
 import java.net.Inet4Address;
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index 464778f..19d70c6 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -114,7 +114,7 @@
 import com.android.net.module.util.ip.IpNeighborMonitor.NeighborEventConsumer;
 import com.android.networkstack.tethering.BpfCoordinator;
 import com.android.networkstack.tethering.BpfCoordinator.ClientInfo;
-import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6DownstreamRule;
 import com.android.networkstack.tethering.PrivateAddressCoordinator;
 import com.android.networkstack.tethering.Tether6Value;
 import com.android.networkstack.tethering.TetherDevKey;
@@ -899,9 +899,9 @@
     }
 
     @NonNull
-    private static Ipv6ForwardingRule makeForwardingRule(
-            int upstreamIfindex, @NonNull InetAddress dst, @NonNull MacAddress dstMac) {
-        return new Ipv6ForwardingRule(upstreamIfindex, TEST_IFACE_PARAMS.index,
+    private static Ipv6DownstreamRule makeDownstreamRule(int upstreamIfindex,
+            @NonNull InetAddress dst, @NonNull MacAddress dstMac) {
+        return new Ipv6DownstreamRule(upstreamIfindex, TEST_IFACE_PARAMS.index,
                 (Inet6Address) dst, TEST_IFACE_PARAMS.macAddr, dstMac);
     }
 
@@ -1064,16 +1064,16 @@
 
         // Events on this interface are received and sent to netd.
         recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA);
-        verify(mBpfCoordinator).tetherOffloadRuleAdd(
-                mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighA, macA));
+        verify(mBpfCoordinator).addIpv6DownstreamRule(
+                mIpServer, makeDownstreamRule(UPSTREAM_IFINDEX, neighA, macA));
         verifyTetherOffloadRuleAdd(null,
                 UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
         verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
         resetNetdBpfMapAndCoordinator();
 
         recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
-        verify(mBpfCoordinator).tetherOffloadRuleAdd(
-                mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighB, macB));
+        verify(mBpfCoordinator).addIpv6DownstreamRule(
+                mIpServer, makeDownstreamRule(UPSTREAM_IFINDEX, neighB, macB));
         verifyTetherOffloadRuleAdd(null,
                 UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
         verifyNoUpstreamIpv6ForwardingChange(null);
@@ -1088,8 +1088,8 @@
         // A neighbor that is no longer valid causes the rule to be removed.
         // NUD_FAILED events do not have a MAC address.
         recvNewNeigh(myIfindex, neighA, NUD_FAILED, null);
-        verify(mBpfCoordinator).tetherOffloadRuleRemove(
-                mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighA, macNull));
+        verify(mBpfCoordinator).removeIpv6DownstreamRule(
+                mIpServer, makeDownstreamRule(UPSTREAM_IFINDEX, neighA, macNull));
         verifyTetherOffloadRuleRemove(null,
                 UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macNull);
         verifyNoUpstreamIpv6ForwardingChange(null);
@@ -1097,8 +1097,8 @@
 
         // A neighbor that is deleted causes the rule to be removed.
         recvDelNeigh(myIfindex, neighB, NUD_STALE, macB);
-        verify(mBpfCoordinator).tetherOffloadRuleRemove(
-                mIpServer,  makeForwardingRule(UPSTREAM_IFINDEX, neighB, macNull));
+        verify(mBpfCoordinator).removeIpv6DownstreamRule(
+                mIpServer,  makeDownstreamRule(UPSTREAM_IFINDEX, neighB, macNull));
         verifyTetherOffloadRuleRemove(null,
                 UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macNull);
         verifyStopUpstreamIpv6Forwarding(null);
@@ -1155,13 +1155,13 @@
         lp.setInterfaceName(UPSTREAM_IFACE);
         dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, -1);
         recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
-        verify(mBpfCoordinator).tetherOffloadRuleAdd(
-                mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighB, macB));
+        verify(mBpfCoordinator).addIpv6DownstreamRule(
+                mIpServer, makeDownstreamRule(UPSTREAM_IFINDEX, neighB, macB));
         verifyTetherOffloadRuleAdd(null,
                 UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
         verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
-        verify(mBpfCoordinator, never()).tetherOffloadRuleAdd(
-                mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighA, macA));
+        verify(mBpfCoordinator, never()).addIpv6DownstreamRule(
+                mIpServer, makeDownstreamRule(UPSTREAM_IFINDEX, neighA, macA));
         verifyNeverTetherOffloadRuleAdd(
                 UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
 
@@ -1178,13 +1178,13 @@
         dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, -1);
         recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA);
         recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
-        verify(mBpfCoordinator).tetherOffloadRuleAdd(
-                mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighA, macA));
+        verify(mBpfCoordinator).addIpv6DownstreamRule(
+                mIpServer, makeDownstreamRule(UPSTREAM_IFINDEX, neighA, macA));
         verifyTetherOffloadRuleAdd(null,
                 UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
         verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
-        verify(mBpfCoordinator).tetherOffloadRuleAdd(
-                mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighB, macB));
+        verify(mBpfCoordinator).addIpv6DownstreamRule(
+                mIpServer, makeDownstreamRule(UPSTREAM_IFINDEX, neighB, macB));
         verifyTetherOffloadRuleAdd(null,
                 UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
         resetNetdBpfMapAndCoordinator();
@@ -1222,16 +1222,16 @@
         resetNetdBpfMapAndCoordinator();
 
         recvNewNeigh(myIfindex, neigh, NUD_REACHABLE, macA);
-        verify(mBpfCoordinator).tetherOffloadRuleAdd(
-                mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neigh, macA));
+        verify(mBpfCoordinator).addIpv6DownstreamRule(
+                mIpServer, makeDownstreamRule(UPSTREAM_IFINDEX, neigh, macA));
         verifyTetherOffloadRuleAdd(null,
                 UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neigh, macA);
         verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
         resetNetdBpfMapAndCoordinator();
 
         recvDelNeigh(myIfindex, neigh, NUD_STALE, macA);
-        verify(mBpfCoordinator).tetherOffloadRuleRemove(
-                mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neigh, macNull));
+        verify(mBpfCoordinator).removeIpv6DownstreamRule(
+                mIpServer, makeDownstreamRule(UPSTREAM_IFINDEX, neigh, macNull));
         verifyTetherOffloadRuleRemove(null,
                 UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neigh, macNull);
         verifyStopUpstreamIpv6Forwarding(null);
@@ -1244,13 +1244,13 @@
         resetNetdBpfMapAndCoordinator();
 
         recvNewNeigh(myIfindex, neigh, NUD_REACHABLE, macA);
-        verify(mBpfCoordinator, never()).tetherOffloadRuleAdd(any(), any());
+        verify(mBpfCoordinator, never()).addIpv6DownstreamRule(any(), any());
         verifyNeverTetherOffloadRuleAdd();
         verifyNoUpstreamIpv6ForwardingChange(null);
         resetNetdBpfMapAndCoordinator();
 
         recvDelNeigh(myIfindex, neigh, NUD_STALE, macA);
-        verify(mBpfCoordinator, never()).tetherOffloadRuleRemove(any(), any());
+        verify(mBpfCoordinator, never()).removeIpv6DownstreamRule(any(), any());
         verifyNeverTetherOffloadRuleRemove();
         verifyNoUpstreamIpv6ForwardingChange(null);
         resetNetdBpfMapAndCoordinator();
@@ -1534,8 +1534,8 @@
         final InetAddress neigh = InetAddresses.parseNumericAddress("2001:db8::1");
         final MacAddress mac = MacAddress.fromString("00:00:00:00:00:0a");
         recvNewNeigh(myIfindex, neigh, NUD_REACHABLE, mac);
-        verify(mBpfCoordinator, never()).tetherOffloadRuleAdd(
-                mIpServer, makeForwardingRule(IPSEC_IFINDEX, neigh, mac));
+        verify(mBpfCoordinator, never()).addIpv6DownstreamRule(
+                mIpServer, makeDownstreamRule(IPSEC_IFINDEX, neigh, mac));
     }
 
     // TODO: move to BpfCoordinatorTest once IpNeighborMonitor is migrated to BpfCoordinator.
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
index 8bc4c18..04eb430 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -118,7 +118,8 @@
 import com.android.net.module.util.netlink.NetlinkUtils;
 import com.android.networkstack.tethering.BpfCoordinator.BpfConntrackEventConsumer;
 import com.android.networkstack.tethering.BpfCoordinator.ClientInfo;
-import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6DownstreamRule;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6UpstreamRule;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
@@ -192,6 +193,7 @@
     private static final Inet4Address XLAT_LOCAL_IPV4ADDR =
             (Inet4Address) InetAddresses.parseNumericAddress("192.0.0.46");
     private static final IpPrefix NAT64_IP_PREFIX = new IpPrefix("64:ff9b::/96");
+    private static final IpPrefix IPV6_ZERO_PREFIX = new IpPrefix("::/64");
 
     // Generally, public port and private port are the same in the NAT conntrack message.
     // TODO: consider using different private port and public port for testing.
@@ -669,8 +671,8 @@
         }
     }
 
-    private void verifyTetherOffloadRuleAdd(@Nullable InOrder inOrder,
-            @NonNull Ipv6ForwardingRule rule) throws Exception {
+    private void verifyAddDownstreamRule(@Nullable InOrder inOrder,
+            @NonNull Ipv6DownstreamRule rule) throws Exception {
         if (mDeps.isAtLeastS()) {
             verifyWithOrder(inOrder, mBpfDownstream6Map).updateEntry(
                     rule.makeTetherDownstream6Key(), rule.makeTether6Value());
@@ -679,7 +681,7 @@
         }
     }
 
-    private void verifyNeverTetherOffloadRuleAdd() throws Exception {
+    private void verifyNeverAddDownstreamRule() throws Exception {
         if (mDeps.isAtLeastS()) {
             verify(mBpfDownstream6Map, never()).updateEntry(any(), any());
         } else {
@@ -687,8 +689,8 @@
         }
     }
 
-    private void verifyTetherOffloadRuleRemove(@Nullable InOrder inOrder,
-            @NonNull final Ipv6ForwardingRule rule) throws Exception {
+    private void verifyRemoveDownstreamRule(@Nullable InOrder inOrder,
+            @NonNull final Ipv6DownstreamRule rule) throws Exception {
         if (mDeps.isAtLeastS()) {
             verifyWithOrder(inOrder, mBpfDownstream6Map).deleteEntry(
                     rule.makeTetherDownstream6Key());
@@ -697,7 +699,7 @@
         }
     }
 
-    private void verifyNeverTetherOffloadRuleRemove() throws Exception {
+    private void verifyNeverRemoveDownstreamRule() throws Exception {
         if (mDeps.isAtLeastS()) {
             verify(mBpfDownstream6Map, never()).deleteEntry(any());
         } else {
@@ -768,17 +770,17 @@
         // The #verifyTetherOffloadGetAndClearStats can't distinguish who has ever called
         // mBpfStatsMap#getValue and get a wrong calling count which counts all.
         final InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfLimitMap, mBpfStatsMap);
-        final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A);
-        coordinator.tetherOffloadRuleAdd(mIpServer, rule);
-        verifyTetherOffloadRuleAdd(inOrder, rule);
+        final Ipv6DownstreamRule rule = buildTestDownstreamRule(mobileIfIndex, NEIGH_A, MAC_A);
+        coordinator.addIpv6DownstreamRule(mIpServer, rule);
+        verifyAddDownstreamRule(inOrder, rule);
         verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
                 true /* isInit */);
 
         // Removing the last rule on current upstream immediately sends the cleanup stuff to netd.
         updateStatsEntryForTetherOffloadGetAndClearStats(
                 buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0));
-        coordinator.tetherOffloadRuleRemove(mIpServer, rule);
-        verifyTetherOffloadRuleRemove(inOrder, rule);
+        coordinator.removeIpv6DownstreamRule(mIpServer, rule);
+        verifyRemoveDownstreamRule(inOrder, rule);
         verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
     }
 
@@ -947,7 +949,7 @@
         public final MacAddress srcMac;
         public final MacAddress dstMac;
 
-        TetherOffloadRuleParcelMatcher(@NonNull Ipv6ForwardingRule rule) {
+        TetherOffloadRuleParcelMatcher(@NonNull Ipv6DownstreamRule rule) {
             upstreamIfindex = rule.upstreamIfindex;
             downstreamIfindex = rule.downstreamIfindex;
             address = rule.address;
@@ -971,21 +973,28 @@
     }
 
     @NonNull
-    private TetherOffloadRuleParcel matches(@NonNull Ipv6ForwardingRule rule) {
+    private TetherOffloadRuleParcel matches(@NonNull Ipv6DownstreamRule rule) {
         return argThat(new TetherOffloadRuleParcelMatcher(rule));
     }
 
     @NonNull
-    private static Ipv6ForwardingRule buildTestForwardingRule(
+    private static Ipv6UpstreamRule buildTestUpstreamRule(int upstreamIfindex) {
+        return new Ipv6UpstreamRule(upstreamIfindex, DOWNSTREAM_IFINDEX,
+                IPV6_ZERO_PREFIX, DOWNSTREAM_MAC, MacAddress.ALL_ZEROS_ADDRESS,
+                MacAddress.ALL_ZEROS_ADDRESS);
+    }
+
+    @NonNull
+    private static Ipv6DownstreamRule buildTestDownstreamRule(
             int upstreamIfindex, @NonNull InetAddress address, @NonNull MacAddress dstMac) {
-        return new Ipv6ForwardingRule(upstreamIfindex, DOWNSTREAM_IFINDEX, (Inet6Address) address,
-                DOWNSTREAM_MAC, dstMac);
+        return new Ipv6DownstreamRule(upstreamIfindex, DOWNSTREAM_IFINDEX,
+                (Inet6Address) address, DOWNSTREAM_MAC, dstMac);
     }
 
     @Test
     public void testRuleMakeTetherDownstream6Key() throws Exception {
         final int mobileIfIndex = 100;
-        final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A);
+        final Ipv6DownstreamRule rule = buildTestDownstreamRule(mobileIfIndex, NEIGH_A, MAC_A);
 
         final TetherDownstream6Key key = rule.makeTetherDownstream6Key();
         assertEquals(key.iif, mobileIfIndex);
@@ -998,7 +1007,7 @@
     @Test
     public void testRuleMakeTether6Value() throws Exception {
         final int mobileIfIndex = 100;
-        final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A);
+        final Ipv6DownstreamRule rule = buildTestDownstreamRule(mobileIfIndex, NEIGH_A, MAC_A);
 
         final Tether6Value value = rule.makeTether6Value();
         assertEquals(value.oif, DOWNSTREAM_IFINDEX);
@@ -1023,10 +1032,10 @@
         // [1] Default limit.
         // Set the unlimited quota as default if the service has never applied a data limit for a
         // given upstream. Note that the data limit only be applied on an upstream which has rules.
-        final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A);
+        final Ipv6DownstreamRule rule = buildTestDownstreamRule(mobileIfIndex, NEIGH_A, MAC_A);
         final InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfLimitMap, mBpfStatsMap);
-        coordinator.tetherOffloadRuleAdd(mIpServer, rule);
-        verifyTetherOffloadRuleAdd(inOrder, rule);
+        coordinator.addIpv6DownstreamRule(mIpServer, rule);
+        verifyAddDownstreamRule(inOrder, rule);
         verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
                 true /* isInit */);
         inOrder.verifyNoMoreInteractions();
@@ -1073,28 +1082,28 @@
         verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
 
         // Adding the first rule on current upstream immediately sends the quota to netd.
-        final Ipv6ForwardingRule ruleA = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A);
-        coordinator.tetherOffloadRuleAdd(mIpServer, ruleA);
-        verifyTetherOffloadRuleAdd(inOrder, ruleA);
+        final Ipv6DownstreamRule ruleA = buildTestDownstreamRule(mobileIfIndex, NEIGH_A, MAC_A);
+        coordinator.addIpv6DownstreamRule(mIpServer, ruleA);
+        verifyAddDownstreamRule(inOrder, ruleA);
         verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, limit, true /* isInit */);
         inOrder.verifyNoMoreInteractions();
 
         // Adding the second rule on current upstream does not send the quota to netd.
-        final Ipv6ForwardingRule ruleB = buildTestForwardingRule(mobileIfIndex, NEIGH_B, MAC_B);
-        coordinator.tetherOffloadRuleAdd(mIpServer, ruleB);
-        verifyTetherOffloadRuleAdd(inOrder, ruleB);
+        final Ipv6DownstreamRule ruleB = buildTestDownstreamRule(mobileIfIndex, NEIGH_B, MAC_B);
+        coordinator.addIpv6DownstreamRule(mIpServer, ruleB);
+        verifyAddDownstreamRule(inOrder, ruleB);
         verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
 
         // Removing the second rule on current upstream does not send the quota to netd.
-        coordinator.tetherOffloadRuleRemove(mIpServer, ruleB);
-        verifyTetherOffloadRuleRemove(inOrder, ruleB);
+        coordinator.removeIpv6DownstreamRule(mIpServer, ruleB);
+        verifyRemoveDownstreamRule(inOrder, ruleB);
         verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
 
         // Removing the last rule on current upstream immediately sends the cleanup stuff to netd.
         updateStatsEntryForTetherOffloadGetAndClearStats(
                 buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0));
-        coordinator.tetherOffloadRuleRemove(mIpServer, ruleA);
-        verifyTetherOffloadRuleRemove(inOrder, ruleA);
+        coordinator.removeIpv6DownstreamRule(mIpServer, ruleA);
+        verifyRemoveDownstreamRule(inOrder, ruleA);
         verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
         inOrder.verifyNoMoreInteractions();
     }
@@ -1124,23 +1133,23 @@
 
         // [1] Adding rules on the upstream Ethernet.
         // Note that the default data limit is applied after the first rule is added.
-        final Ipv6ForwardingRule ethernetRuleA = buildTestForwardingRule(
+        final Ipv6DownstreamRule ethernetRuleA = buildTestDownstreamRule(
                 ethIfIndex, NEIGH_A, MAC_A);
-        final Ipv6ForwardingRule ethernetRuleB = buildTestForwardingRule(
+        final Ipv6DownstreamRule ethernetRuleB = buildTestDownstreamRule(
                 ethIfIndex, NEIGH_B, MAC_B);
 
-        coordinator.tetherOffloadRuleAdd(mIpServer, ethernetRuleA);
-        verifyTetherOffloadRuleAdd(inOrder, ethernetRuleA);
+        coordinator.addIpv6DownstreamRule(mIpServer, ethernetRuleA);
+        verifyAddDownstreamRule(inOrder, ethernetRuleA);
         verifyTetherOffloadSetInterfaceQuota(inOrder, ethIfIndex, QUOTA_UNLIMITED,
                 true /* isInit */);
         verifyStartUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC, ethIfIndex);
-        coordinator.tetherOffloadRuleAdd(mIpServer, ethernetRuleB);
-        verifyTetherOffloadRuleAdd(inOrder, ethernetRuleB);
+        coordinator.addIpv6DownstreamRule(mIpServer, ethernetRuleB);
+        verifyAddDownstreamRule(inOrder, ethernetRuleB);
 
         // [2] Update the existing rules from Ethernet to cellular.
-        final Ipv6ForwardingRule mobileRuleA = buildTestForwardingRule(
+        final Ipv6DownstreamRule mobileRuleA = buildTestDownstreamRule(
                 mobileIfIndex, NEIGH_A, MAC_A);
-        final Ipv6ForwardingRule mobileRuleB = buildTestForwardingRule(
+        final Ipv6DownstreamRule mobileRuleB = buildTestDownstreamRule(
                 mobileIfIndex, NEIGH_B, MAC_B);
         updateStatsEntryForTetherOffloadGetAndClearStats(
                 buildTestTetherStatsParcel(ethIfIndex, 10, 20, 30, 40));
@@ -1148,23 +1157,23 @@
         // Update the existing rules for upstream changes. The rules are removed and re-added one
         // by one for updating upstream interface index by #tetherOffloadRuleUpdate.
         coordinator.tetherOffloadRuleUpdate(mIpServer, mobileIfIndex);
-        verifyTetherOffloadRuleRemove(inOrder, ethernetRuleA);
-        verifyTetherOffloadRuleRemove(inOrder, ethernetRuleB);
+        verifyRemoveDownstreamRule(inOrder, ethernetRuleA);
+        verifyRemoveDownstreamRule(inOrder, ethernetRuleB);
         verifyStopUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC);
         verifyTetherOffloadGetAndClearStats(inOrder, ethIfIndex);
-        verifyTetherOffloadRuleAdd(inOrder, mobileRuleA);
+        verifyAddDownstreamRule(inOrder, mobileRuleA);
         verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
                 true /* isInit */);
         verifyStartUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC,
                 mobileIfIndex);
-        verifyTetherOffloadRuleAdd(inOrder, mobileRuleB);
+        verifyAddDownstreamRule(inOrder, mobileRuleB);
 
         // [3] Clear all rules for a given IpServer.
         updateStatsEntryForTetherOffloadGetAndClearStats(
                 buildTestTetherStatsParcel(mobileIfIndex, 50, 60, 70, 80));
         coordinator.tetherOffloadRuleClear(mIpServer);
-        verifyTetherOffloadRuleRemove(inOrder, mobileRuleA);
-        verifyTetherOffloadRuleRemove(inOrder, mobileRuleB);
+        verifyRemoveDownstreamRule(inOrder, mobileRuleA);
+        verifyRemoveDownstreamRule(inOrder, mobileRuleB);
         verifyStopUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC);
         verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
 
@@ -1201,37 +1210,37 @@
         // The rule can't be added.
         final InetAddress neigh = InetAddresses.parseNumericAddress("2001:db8::1");
         final MacAddress mac = MacAddress.fromString("00:00:00:00:00:0a");
-        final Ipv6ForwardingRule rule = buildTestForwardingRule(ifIndex, neigh, mac);
-        coordinator.tetherOffloadRuleAdd(mIpServer, rule);
-        verifyNeverTetherOffloadRuleAdd();
-        LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules =
-                coordinator.getForwardingRulesForTesting().get(mIpServer);
+        final Ipv6DownstreamRule rule = buildTestDownstreamRule(ifIndex, neigh, mac);
+        coordinator.addIpv6DownstreamRule(mIpServer, rule);
+        verifyNeverAddDownstreamRule();
+        LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules =
+                coordinator.getIpv6DownstreamRulesForTesting().get(mIpServer);
         assertNull(rules);
 
         // The rule can't be removed. This is not a realistic case because adding rule is not
         // allowed. That implies no rule could be removed, cleared or updated. Verify these
         // cases just in case.
-        rules = new LinkedHashMap<Inet6Address, Ipv6ForwardingRule>();
+        rules = new LinkedHashMap<Inet6Address, Ipv6DownstreamRule>();
         rules.put(rule.address, rule);
-        coordinator.getForwardingRulesForTesting().put(mIpServer, rules);
-        coordinator.tetherOffloadRuleRemove(mIpServer, rule);
-        verifyNeverTetherOffloadRuleRemove();
-        rules = coordinator.getForwardingRulesForTesting().get(mIpServer);
+        coordinator.getIpv6DownstreamRulesForTesting().put(mIpServer, rules);
+        coordinator.removeIpv6DownstreamRule(mIpServer, rule);
+        verifyNeverRemoveDownstreamRule();
+        rules = coordinator.getIpv6DownstreamRulesForTesting().get(mIpServer);
         assertNotNull(rules);
         assertEquals(1, rules.size());
 
         // The rule can't be cleared.
         coordinator.tetherOffloadRuleClear(mIpServer);
-        verifyNeverTetherOffloadRuleRemove();
-        rules = coordinator.getForwardingRulesForTesting().get(mIpServer);
+        verifyNeverRemoveDownstreamRule();
+        rules = coordinator.getIpv6DownstreamRulesForTesting().get(mIpServer);
         assertNotNull(rules);
         assertEquals(1, rules.size());
 
         // The rule can't be updated.
         coordinator.tetherOffloadRuleUpdate(mIpServer, rule.upstreamIfindex + 1 /* new */);
-        verifyNeverTetherOffloadRuleRemove();
-        verifyNeverTetherOffloadRuleAdd();
-        rules = coordinator.getForwardingRulesForTesting().get(mIpServer);
+        verifyNeverRemoveDownstreamRule();
+        verifyNeverAddDownstreamRule();
+        rules = coordinator.getIpv6DownstreamRulesForTesting().get(mIpServer);
         assertNotNull(rules);
         assertEquals(1, rules.size());
     }
@@ -1669,17 +1678,17 @@
         final BpfCoordinator coordinator = makeBpfCoordinator();
 
         coordinator.addUpstreamNameToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE);
-        final Ipv6ForwardingRule ruleA = buildTestForwardingRule(UPSTREAM_IFINDEX, NEIGH_A, MAC_A);
-        final Ipv6ForwardingRule ruleB = buildTestForwardingRule(UPSTREAM_IFINDEX, NEIGH_B, MAC_B);
+        final Ipv6DownstreamRule ruleA = buildTestDownstreamRule(UPSTREAM_IFINDEX, NEIGH_A, MAC_A);
+        final Ipv6DownstreamRule ruleB = buildTestDownstreamRule(UPSTREAM_IFINDEX, NEIGH_B, MAC_B);
 
-        coordinator.tetherOffloadRuleAdd(mIpServer, ruleA);
+        coordinator.addIpv6DownstreamRule(mIpServer, ruleA);
         verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(UPSTREAM_IFINDEX)),
                 eq(new TetherDevValue(UPSTREAM_IFINDEX)));
         verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(DOWNSTREAM_IFINDEX)),
                 eq(new TetherDevValue(DOWNSTREAM_IFINDEX)));
         clearInvocations(mBpfDevMap);
 
-        coordinator.tetherOffloadRuleAdd(mIpServer, ruleB);
+        coordinator.addIpv6DownstreamRule(mIpServer, ruleB);
         verify(mBpfDevMap, never()).updateEntry(any(), any());
     }
 
@@ -2139,9 +2148,15 @@
 
     @Test
     public void testIpv6ForwardingRuleToString() throws Exception {
-        final Ipv6ForwardingRule rule = buildTestForwardingRule(UPSTREAM_IFINDEX, NEIGH_A, MAC_A);
+        final Ipv6DownstreamRule downstreamRule = buildTestDownstreamRule(UPSTREAM_IFINDEX, NEIGH_A,
+                MAC_A);
         assertEquals("upstreamIfindex: 1001, downstreamIfindex: 2001, address: 2001:db8::1, "
-                + "srcMac: 12:34:56:78:90:ab, dstMac: 00:00:00:00:00:0a", rule.toString());
+                + "srcMac: 12:34:56:78:90:ab, dstMac: 00:00:00:00:00:0a",
+                downstreamRule.toString());
+        final Ipv6UpstreamRule upstreamRule = buildTestUpstreamRule(UPSTREAM_IFINDEX);
+        assertEquals("upstreamIfindex: 1001, downstreamIfindex: 2001, sourcePrefix: ::/64, "
+                + "inDstMac: 12:34:56:78:90:ab, outSrcMac: 00:00:00:00:00:00, "
+                + "outDstMac: 00:00:00:00:00:00", upstreamRule.toString());
     }
 
     private void verifyDump(@NonNull final BpfCoordinator coordinator) {
@@ -2177,7 +2192,7 @@
         // - dumpCounters
         //   * mBpfErrorMap
         // - dumpIpv6ForwardingRulesByDownstream
-        //   * mIpv6ForwardingRules
+        //   * mIpv6DownstreamRules
 
         // dumpBpfForwardingRulesIpv4
         mBpfDownstream4Map.insertEntry(
@@ -2188,7 +2203,7 @@
                 new TestUpstream4Value.Builder().build());
 
         // dumpBpfForwardingRulesIpv6
-        final Ipv6ForwardingRule rule = buildTestForwardingRule(UPSTREAM_IFINDEX, NEIGH_A, MAC_A);
+        final Ipv6DownstreamRule rule = buildTestDownstreamRule(UPSTREAM_IFINDEX, NEIGH_A, MAC_A);
         mBpfDownstream6Map.insertEntry(rule.makeTetherDownstream6Key(), rule.makeTether6Value());
 
         final TetherUpstream6Key upstream6Key = new TetherUpstream6Key(DOWNSTREAM_IFINDEX,
@@ -2218,12 +2233,12 @@
                 new S32(1000 /* count */));
 
         // dumpIpv6ForwardingRulesByDownstream
-        final HashMap<IpServer, LinkedHashMap<Inet6Address, Ipv6ForwardingRule>>
-                ipv6ForwardingRules = coordinator.getForwardingRulesForTesting();
-        final LinkedHashMap<Inet6Address, Ipv6ForwardingRule> addressRuleMap =
+        final HashMap<IpServer, LinkedHashMap<Inet6Address, Ipv6DownstreamRule>>
+                ipv6DownstreamRules = coordinator.getIpv6DownstreamRulesForTesting();
+        final LinkedHashMap<Inet6Address, Ipv6DownstreamRule> addressRuleMap =
                 new LinkedHashMap<>();
         addressRuleMap.put(rule.address, rule);
-        ipv6ForwardingRules.put(mIpServer, addressRuleMap);
+        ipv6DownstreamRules.put(mIpServer, addressRuleMap);
 
         verifyDump(coordinator);
     }
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 b1f875b..4413d26 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java
@@ -20,6 +20,8 @@
 import static android.system.OsConstants.AF_UNIX;
 import static android.system.OsConstants.SOCK_STREAM;
 
+import static com.android.networkstack.tethering.OffloadHardwareInterface.NF_NETLINK_CONNTRACK_DESTROY;
+import static com.android.networkstack.tethering.OffloadHardwareInterface.NF_NETLINK_CONNTRACK_NEW;
 import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_AIDL;
 import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_HIDL_1_0;
 import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_HIDL_1_1;
@@ -34,6 +36,7 @@
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -57,7 +60,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
 import java.io.FileDescriptor;
@@ -75,8 +77,9 @@
     private OffloadHardwareInterface mOffloadHw;
     private OffloadHalCallback mOffloadHalCallback;
 
-    @Mock private IOffloadHal mIOffload;
-    @Mock private NativeHandle mNativeHandle;
+    private IOffloadHal mIOffload;
+    private NativeHandle mNativeHandle1;
+    private NativeHandle mNativeHandle2;
 
     // Random values to test Netlink message.
     private static final short TEST_TYPE = 184;
@@ -97,7 +100,9 @@
 
         @Override
         public NativeHandle createConntrackSocket(final int groups) {
-            return mNativeHandle;
+            return groups == (NF_NETLINK_CONNTRACK_NEW | NF_NETLINK_CONNTRACK_DESTROY)
+                    ? mNativeHandle1
+                    : mNativeHandle2;
         }
     }
 
@@ -105,45 +110,89 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         mOffloadHalCallback = new OffloadHalCallback();
-        when(mIOffload.initOffload(any(NativeHandle.class), any(NativeHandle.class),
-                any(OffloadHalCallback.class))).thenReturn(true);
+        mIOffload = mock(IOffloadHal.class);
     }
 
-    private void startOffloadHardwareInterface(int offloadHalVersion)
+    private void startOffloadHardwareInterface(int offloadHalVersion, boolean isHalInitSuccess)
             throws Exception {
+        startOffloadHardwareInterface(offloadHalVersion, isHalInitSuccess, mock(NativeHandle.class),
+                mock(NativeHandle.class));
+    }
+
+    private void startOffloadHardwareInterface(int offloadHalVersion, boolean isHalInitSuccess,
+            NativeHandle handle1, NativeHandle handle2) throws Exception {
         final SharedLog log = new SharedLog("test");
         final Handler handler = new Handler(mTestLooper.getLooper());
-        final int num = offloadHalVersion != OFFLOAD_HAL_VERSION_NONE ? 1 : 0;
+        final boolean hasNullHandle = handle1 == null || handle2 == null;
+        // If offloadHalVersion is OFFLOAD_HAL_VERSION_NONE or it has null NativeHandle arguments,
+        // mIOffload.initOffload() shouldn't be called.
+        final int initNum = (offloadHalVersion != OFFLOAD_HAL_VERSION_NONE && !hasNullHandle)
+                ? 1
+                : 0;
+        // If it is HIDL or has null NativeHandle argument, NativeHandles should be closed.
+        final int handleCloseNum = (hasNullHandle
+                || offloadHalVersion == OFFLOAD_HAL_VERSION_HIDL_1_0
+                || offloadHalVersion == OFFLOAD_HAL_VERSION_HIDL_1_1) ? 1 : 0;
+        mNativeHandle1 = handle1;
+        mNativeHandle2 = handle2;
+        when(mIOffload.initOffload(any(NativeHandle.class), any(NativeHandle.class),
+                any(OffloadHalCallback.class))).thenReturn(isHalInitSuccess);
         mOffloadHw = new OffloadHardwareInterface(handler, log,
                 new MyDependencies(handler, log, offloadHalVersion));
-        assertEquals(offloadHalVersion, mOffloadHw.initOffload(mOffloadHalCallback));
-        verify(mIOffload, times(num)).initOffload(any(NativeHandle.class), any(NativeHandle.class),
-                eq(mOffloadHalCallback));
+        assertEquals(isHalInitSuccess && !hasNullHandle
+                ? offloadHalVersion
+                : OFFLOAD_HAL_VERSION_NONE,
+                mOffloadHw.initOffload(mOffloadHalCallback));
+        verify(mIOffload, times(initNum)).initOffload(any(NativeHandle.class),
+                any(NativeHandle.class), eq(mOffloadHalCallback));
+        if (mNativeHandle1 != null) verify(mNativeHandle1, times(handleCloseNum)).close();
+        if (mNativeHandle2 != null) verify(mNativeHandle2, times(handleCloseNum)).close();
     }
 
     @Test
     public void testInitFailureWithNoHal() throws Exception {
-        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_NONE);
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_NONE, true);
     }
 
     @Test
     public void testInitSuccessWithAidl() throws Exception {
-        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_AIDL);
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_AIDL, true);
     }
 
     @Test
     public void testInitSuccessWithHidl_1_0() throws Exception {
-        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0);
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0, true);
     }
 
     @Test
     public void testInitSuccessWithHidl_1_1() throws Exception {
-        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_1);
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_1, true);
+    }
+
+    @Test
+    public void testInitFailWithAidl() throws Exception {
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_AIDL, false);
+    }
+
+    @Test
+    public void testInitFailWithHidl_1_0() throws Exception {
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0, false);
+    }
+
+    @Test
+    public void testInitFailWithHidl_1_1() throws Exception {
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_1, false);
+    }
+
+    @Test
+    public void testInitFailDueToNullHandles() throws Exception {
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_AIDL, true, mock(NativeHandle.class),
+                null);
     }
 
     @Test
     public void testGetForwardedStats() throws Exception {
-        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0);
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0, true);
         ForwardedStats stats = new ForwardedStats(12345, 56780);
         when(mIOffload.getForwardedStats(anyString())).thenReturn(stats);
         assertEquals(mOffloadHw.getForwardedStats(RMNET0), stats);
@@ -152,7 +201,7 @@
 
     @Test
     public void testSetLocalPrefixes() throws Exception {
-        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0);
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0, true);
         final ArrayList<String> localPrefixes = new ArrayList<>();
         localPrefixes.add("127.0.0.0/8");
         localPrefixes.add("fe80::/64");
@@ -165,7 +214,7 @@
 
     @Test
     public void testSetDataLimit() throws Exception {
-        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0);
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0, true);
         final long limit = 12345;
         when(mIOffload.setDataLimit(anyString(), anyLong())).thenReturn(true);
         assertTrue(mOffloadHw.setDataLimit(RMNET0, limit));
@@ -177,7 +226,7 @@
     @Test
     public void testSetDataWarningAndLimitFailureWithHidl_1_0() throws Exception {
         // Verify V1.0 control HAL would reject the function call with exception.
-        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0);
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0, true);
         final long warning = 12345;
         final long limit = 67890;
         assertThrows(UnsupportedOperationException.class,
@@ -187,7 +236,7 @@
     @Test
     public void testSetDataWarningAndLimit() throws Exception {
         // Verify V1.1 control HAL could receive this function call.
-        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_1);
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_1, true);
         final long warning = 12345;
         final long limit = 67890;
         when(mIOffload.setDataWarningAndLimit(anyString(), anyLong(), anyLong())).thenReturn(true);
@@ -199,7 +248,7 @@
 
     @Test
     public void testSetUpstreamParameters() throws Exception {
-        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0);
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0, true);
         final String v4addr = "192.168.10.1";
         final String v4gateway = "192.168.10.255";
         final ArrayList<String> v6gws = new ArrayList<>(0);
@@ -220,7 +269,7 @@
 
     @Test
     public void testUpdateDownstream() throws Exception {
-        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0);
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0, true);
         final String ifName = "wlan1";
         final String prefix = "192.168.43.0/24";
         when(mIOffload.addDownstream(anyString(), anyString())).thenReturn(true);
@@ -237,7 +286,7 @@
 
     @Test
     public void testSendIpv4NfGenMsg() throws Exception {
-        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0);
+        startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_HIDL_1_0, true);
         FileDescriptor writeSocket = new FileDescriptor();
         FileDescriptor readSocket = new FileDescriptor();
         try {
@@ -246,9 +295,9 @@
             fail();
             return;
         }
-        when(mNativeHandle.getFileDescriptor()).thenReturn(writeSocket);
+        when(mNativeHandle1.getFileDescriptor()).thenReturn(writeSocket);
 
-        mOffloadHw.sendIpv4NfGenMsg(mNativeHandle, TEST_TYPE, TEST_FLAGS);
+        mOffloadHw.sendIpv4NfGenMsg(mNativeHandle1, TEST_TYPE, TEST_FLAGS);
 
         ByteBuffer buffer = ByteBuffer.allocate(9823);  // Arbitrary value > expectedLen.
         buffer.order(ByteOrder.nativeOrder());
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 9c6904d..770507e 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -68,6 +68,7 @@
 import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
 import static com.android.net.module.util.Inet4AddressUtils.inet4AddressToIntHTH;
 import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
+import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH;
 import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_HIDL_1_0;
 import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_NONE;
 import static com.android.networkstack.tethering.TestConnectivityManager.BROADCAST_FIRST;
@@ -157,7 +158,6 @@
 import android.net.ip.DadProxy;
 import android.net.ip.IpServer;
 import android.net.ip.RouterAdvertisementDaemon;
-import android.net.util.NetworkConstants;
 import android.net.wifi.SoftApConfiguration;
 import android.net.wifi.WifiClient;
 import android.net.wifi.WifiManager;
@@ -559,7 +559,7 @@
             prop.addDnsServer(InetAddresses.parseNumericAddress("2001:db8::2"));
             prop.addLinkAddress(
                     new LinkAddress(InetAddresses.parseNumericAddress("2001:db8::"),
-                            NetworkConstants.RFC7421_PREFIX_LENGTH));
+                            RFC7421_PREFIX_LENGTH));
             prop.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0),
                     InetAddresses.parseNumericAddress("2001:db8::1"),
                     interfaceName, RTN_UNICAST));
diff --git a/bpf_progs/netd.h b/bpf_progs/netd.h
index be604f9..dcf6d6a 100644
--- a/bpf_progs/netd.h
+++ b/bpf_progs/netd.h
@@ -55,21 +55,22 @@
 } StatsValue;
 STRUCT_SIZE(StatsValue, 4 * 8);  // 32
 
+#ifdef __cplusplus
+static inline StatsValue& operator+=(StatsValue& lhs, const StatsValue& rhs) {
+    lhs.rxPackets += rhs.rxPackets;
+    lhs.rxBytes += rhs.rxBytes;
+    lhs.txPackets += rhs.txPackets;
+    lhs.txBytes += rhs.txBytes;
+    return lhs;
+}
+#endif
+
 typedef struct {
     char name[IFNAMSIZ];
 } IfaceValue;
 STRUCT_SIZE(IfaceValue, 16);
 
 typedef struct {
-    uint64_t rxBytes;
-    uint64_t rxPackets;
-    uint64_t txBytes;
-    uint64_t txPackets;
-    uint64_t tcpRxPackets;
-    uint64_t tcpTxPackets;
-} Stats;
-
-typedef struct {
   uint64_t timestampNs;
   uint32_t ifindex;
   uint32_t length;
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index 9520ef6..dacdaf2 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -42,6 +42,8 @@
     srcs: [
         ":framework-connectivity-tiramisu-updatable-sources",
         ":framework-nearby-java-sources",
+        ":framework-thread-sources",
+        ":framework-remoteauth-java-sources",
     ],
     libs: [
         "unsupportedappusage",
@@ -129,8 +131,10 @@
         "android.net",
         "android.net.nsd",
         "android.nearby",
+        "android.remoteauth",
         "com.android.connectivity",
         "com.android.nearby",
+        "com.android.remoteauth",
     ],
 
     hidden_api: {
@@ -152,6 +156,7 @@
         "//packages/modules/Connectivity/service", // For R8 only
         "//packages/modules/Connectivity/service-t",
         "//packages/modules/Connectivity/nearby:__subpackages__",
+        "//packages/modules/Connectivity/remoteauth:__subpackages__",
         "//frameworks/base",
 
         // Tests using hidden APIs
diff --git a/framework-t/api/OWNERS b/framework-t/api/OWNERS
index de0f905..af583c3 100644
--- a/framework-t/api/OWNERS
+++ b/framework-t/api/OWNERS
@@ -1 +1,2 @@
 file:platform/packages/modules/Connectivity:master:/nearby/OWNERS
+file:platform/packages/modules/Connectivity:master:/remoteauth/OWNERS
diff --git a/framework-t/api/module-lib-current.txt b/framework-t/api/module-lib-current.txt
index 5a8d47b..42c83d8 100644
--- a/framework-t/api/module-lib-current.txt
+++ b/framework-t/api/module-lib-current.txt
@@ -207,3 +207,43 @@
 
 }
 
+package android.remoteauth {
+
+  public interface DeviceDiscoveryCallback {
+    method public void onDeviceUpdate(@NonNull android.remoteauth.RemoteDevice, int);
+    method public void onTimeout();
+    field public static final int STATE_LOST = 0; // 0x0
+    field public static final int STATE_SEEN = 1; // 0x1
+  }
+
+  public final class RemoteAuthFrameworkInitializer {
+    method public static void registerServiceWrappers();
+  }
+
+  public class RemoteAuthManager {
+    method public boolean isRemoteAuthSupported();
+    method public boolean startDiscovery(int, @NonNull java.util.concurrent.Executor, @NonNull android.remoteauth.DeviceDiscoveryCallback);
+    method public void stopDiscovery(@NonNull android.remoteauth.DeviceDiscoveryCallback);
+  }
+
+  public final class RemoteDevice implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public int getConnectionId();
+    method @Nullable public String getName();
+    method public int getRegistrationState();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.remoteauth.RemoteDevice> CREATOR;
+    field public static final int STATE_NOT_REGISTERED = 0; // 0x0
+    field public static final int STATE_REGISTERED = 1; // 0x1
+  }
+
+  public static final class RemoteDevice.Builder {
+    ctor public RemoteDevice.Builder(int);
+    method @NonNull public android.remoteauth.RemoteDevice build();
+    method @NonNull public android.remoteauth.RemoteDevice.Builder setConnectionId(int);
+    method @NonNull public android.remoteauth.RemoteDevice.Builder setName(@Nullable String);
+    method @NonNull public android.remoteauth.RemoteDevice.Builder setRegistrationState(int);
+  }
+
+}
+
diff --git a/framework-t/src/android/net/TrafficStats.java b/framework-t/src/android/net/TrafficStats.java
index dc4ac55..a69b38d 100644
--- a/framework-t/src/android/net/TrafficStats.java
+++ b/framework-t/src/android/net/TrafficStats.java
@@ -683,33 +683,13 @@
     /** {@hide} */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public static long getMobileTcpRxPackets() {
-        long total = 0;
-        for (String iface : getMobileIfaces()) {
-            long stat = UNSUPPORTED;
-            try {
-                stat = getStatsService().getIfaceStats(iface, TYPE_TCP_RX_PACKETS);
-            } catch (RemoteException e) {
-                throw e.rethrowFromSystemServer();
-            }
-            total += addIfSupported(stat);
-        }
-        return total;
+        return UNSUPPORTED;
     }
 
     /** {@hide} */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public static long getMobileTcpTxPackets() {
-        long total = 0;
-        for (String iface : getMobileIfaces()) {
-            long stat = UNSUPPORTED;
-            try {
-                stat = getStatsService().getIfaceStats(iface, TYPE_TCP_TX_PACKETS);
-            } catch (RemoteException e) {
-                throw e.rethrowFromSystemServer();
-            }
-            total += addIfSupported(stat);
-        }
-        return total;
+        return UNSUPPORTED;
     }
 
     /**
@@ -1141,8 +1121,4 @@
     public static final int TYPE_TX_BYTES = 2;
     /** {@hide} */
     public static final int TYPE_TX_PACKETS = 3;
-    /** {@hide} */
-    public static final int TYPE_TCP_RX_PACKETS = 4;
-    /** {@hide} */
-    public static final int TYPE_TCP_TX_PACKETS = 5;
 }
diff --git a/framework/Android.bp b/framework/Android.bp
index 7897492..813e296 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -62,7 +62,6 @@
         ":framework-connectivity-sources",
         ":net-utils-framework-common-srcs",
         ":framework-connectivity-api-shared-srcs",
-        ":framework-remoteauth-java-sources",
     ],
     aidl: {
         generate_get_transaction_name: true,
@@ -112,6 +111,7 @@
     static_libs: [
         "httpclient_api",
         "httpclient_impl",
+        "http_client_logging",
     ],
     libs: [
         // This cannot be in the defaults clause above because if it were, it would be used
@@ -132,6 +132,7 @@
     ],
     impl_only_static_libs: [
         "httpclient_impl",
+        "http_client_logging",
     ],
 }
 
@@ -151,7 +152,6 @@
         "//packages/modules/Connectivity/framework-t",
         "//packages/modules/Connectivity/service",
         "//packages/modules/Connectivity/service-t",
-        "//packages/modules/Connectivity/remoteauth:__subpackages__",
         "//frameworks/base/packages/Connectivity/service",
         "//frameworks/base",
 
diff --git a/netd/BpfHandler.cpp b/netd/BpfHandler.cpp
index d239277..73feee4 100644
--- a/netd/BpfHandler.cpp
+++ b/netd/BpfHandler.cpp
@@ -210,8 +210,8 @@
     };
     auto configuration = mConfigurationMap.readValue(CURRENT_STATS_MAP_CONFIGURATION_KEY);
     if (!configuration.ok()) {
-        ALOGE("Failed to get current configuration: %s, fd: %d",
-              strerror(configuration.error().code()), mConfigurationMap.getMap().get());
+        ALOGE("Failed to get current configuration: %s",
+              strerror(configuration.error().code()));
         return -configuration.error().code();
     }
     if (configuration.value() != SELECT_MAP_A && configuration.value() != SELECT_MAP_B) {
@@ -224,7 +224,7 @@
     // HACK: mStatsMapB becomes RW BpfMap here, but countUidStatsEntries doesn't modify so it works
     base::Result<void> res = currentMap.iterate(countUidStatsEntries);
     if (!res.ok()) {
-        ALOGE("Failed to count the stats entry in map %d: %s", currentMap.getMap().get(),
+        ALOGE("Failed to count the stats entry in map: %s",
               strerror(res.error().code()));
         return -res.error().code();
     }
@@ -243,8 +243,7 @@
     // should be fine to concurrently update the map while eBPF program is running.
     res = mCookieTagMap.writeValue(sock_cookie, newKey, BPF_ANY);
     if (!res.ok()) {
-        ALOGE("Failed to tag the socket: %s, fd: %d", strerror(res.error().code()),
-              mCookieTagMap.getMap().get());
+        ALOGE("Failed to tag the socket: %s", strerror(res.error().code()));
         return -res.error().code();
     }
     ALOGD("Socket with cookie %" PRIu64 " tagged successfully with tag %" PRIu32 " uid %u "
diff --git a/remoteauth/README.md b/remoteauth/README.md
index 384fcf7..f28154d 100644
--- a/remoteauth/README.md
+++ b/remoteauth/README.md
@@ -23,7 +23,6 @@
 
 ### AIDEGen
 
-AIDEGen is deprecated, prefer ASfP [go/asfp](http://go/asfp)
 ```sh
 $ source build/envsetup.sh && lunch <TARGET>
 $ cd packages/modules/Connectivity
@@ -31,17 +30,13 @@
 # This will launch Intellij project for RemoteAuth module.
 ```
 
-### ASfP
-
-See full instructions for ASfP at [go/asfp-getting-started](http://go/asfp-getting-started)
-
 ## Build and Install
 
 ```sh
 $ source build/envsetup.sh && lunch <TARGET>
-$ m com.google.android.tethering deapexer
+$ m com.android.tethering deapexer
 $ $ANDROID_BUILD_TOP/out/host/linux-x86/bin/deapexer decompress --input \
-    ${ANDROID_PRODUCT_OUT}/system/apex/com.google.android.tethering.capex \
+    ${ANDROID_PRODUCT_OUT}/system/apex/com.android.tethering.capex \
     --output /tmp/tethering.apex
 $ adb install -r /tmp/tethering.apex
 ```
diff --git a/remoteauth/TEST_MAPPING b/remoteauth/TEST_MAPPING
index 5ad8da6..5061319 100644
--- a/remoteauth/TEST_MAPPING
+++ b/remoteauth/TEST_MAPPING
@@ -7,7 +7,7 @@
   // TODO(b/193602229): uncomment once it's supported.
   //"mainline-presubmit": [
   //  {
-  //    "name": "RemoteAuthUnitTests[com.google.android.tethering.apex]"
+  //    "name": "RemoteAuthUnitTests[com.google.android.remoteauth.apex]"
   //  }
   //]
 }
diff --git a/remoteauth/framework/Android.bp b/remoteauth/framework/Android.bp
index 48d10b6..71b621a 100644
--- a/remoteauth/framework/Android.bp
+++ b/remoteauth/framework/Android.bp
@@ -25,7 +25,7 @@
     ],
     path: "java",
     visibility: [
-        "//packages/modules/Connectivity/framework:__subpackages__",
+        "//packages/modules/Connectivity/framework-t:__subpackages__",
     ],
 }
 
@@ -43,7 +43,13 @@
     name: "framework-remoteauth-static",
     srcs: [":framework-remoteauth-java-sources"],
     sdk_version: "module_current",
-    libs: [],
-    static_libs: [],
+    libs: [
+        "androidx.annotation_annotation",
+        "framework-annotations-lib",
+        "framework-bluetooth",
+    ],
+    static_libs: [
+        "modules-utils-preconditions",
+    ],
     visibility: ["//packages/modules/Connectivity/remoteauth/tests:__subpackages__"],
 }
diff --git a/remoteauth/framework/java/android/remoteauth/DeviceDiscoveryCallback.java b/remoteauth/framework/java/android/remoteauth/DeviceDiscoveryCallback.java
new file mode 100644
index 0000000..f53e2dc
--- /dev/null
+++ b/remoteauth/framework/java/android/remoteauth/DeviceDiscoveryCallback.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2023 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.remoteauth;
+
+import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+
+import androidx.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Reports newly discovered remote devices.
+ *
+ * @hide
+ */
+@SystemApi(client = MODULE_LIBRARIES)
+public interface DeviceDiscoveryCallback {
+    /** The device is no longer seen in the discovery process. */
+    int STATE_LOST = 0;
+    /** The device is seen in the discovery process */
+    int STATE_SEEN = 1;
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({STATE_LOST, STATE_SEEN})
+    @interface State {}
+
+    /**
+     * Invoked for every change in remote device state.
+     *
+     * @param device remote device
+     * @param state indicates if found or lost
+     */
+    void onDeviceUpdate(@NonNull RemoteDevice device, @State int state);
+
+    /** Invoked when discovery is stopped due to timeout. */
+    void onTimeout();
+}
diff --git a/remoteauth/framework/java/android/remoteauth/IDeviceDiscoveryListener.aidl b/remoteauth/framework/java/android/remoteauth/IDeviceDiscoveryListener.aidl
new file mode 100644
index 0000000..2ad6a6a
--- /dev/null
+++ b/remoteauth/framework/java/android/remoteauth/IDeviceDiscoveryListener.aidl
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2023 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.remoteauth;
+
+import android.remoteauth.RemoteDevice;
+
+/**
+ * Binder callback for DeviceDiscoveryCallback.
+ *
+ * {@hide}
+ */
+oneway interface IDeviceDiscoveryListener {
+        /** Reports a {@link RemoteDevice} being discovered. */
+        void onDiscovered(in RemoteDevice remoteDevice);
+
+        /** Reports a {@link RemoteDevice} is no longer within range. */
+        void onLost(in RemoteDevice remoteDevice);
+
+        /** Reports a timeout of {@link RemoteDevice} was reached. */
+        void onTimeout();
+}
diff --git a/remoteauth/framework/java/android/remoteauth/IRemoteAuthService.aidl b/remoteauth/framework/java/android/remoteauth/IRemoteAuthService.aidl
new file mode 100644
index 0000000..f4387e3
--- /dev/null
+++ b/remoteauth/framework/java/android/remoteauth/IRemoteAuthService.aidl
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 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.remoteauth;
+
+import android.remoteauth.IDeviceDiscoveryListener;
+
+/**
+ * Interface for communicating with the RemoteAuthService.
+ * These methods are all require MANAGE_REMOTE_AUTH signature permission.
+ * @hide
+ */
+interface IRemoteAuthService {
+    // This is protected by the MANAGE_REMOTE_AUTH signature permission.
+    boolean isRemoteAuthSupported();
+
+    // This is protected by the MANAGE_REMOTE_AUTH signature permission.
+    boolean registerDiscoveryListener(in IDeviceDiscoveryListener deviceDiscoveryListener,
+                                  int userId,
+                                  int timeoutMs,
+                                  String packageName,
+                                  @nullable String attributionTag);
+
+    // This is protected by the MANAGE_REMOTE_AUTH signature permission.
+    void unregisterDiscoveryListener(in IDeviceDiscoveryListener deviceDiscoveryListener,
+                                     int userId,
+                                     String packageName,
+                                     @nullable String attributionTag);
+}
\ No newline at end of file
diff --git a/remoteauth/framework/java/android/remoteauth/RemoteAuthFrameworkInitializer.java b/remoteauth/framework/java/android/remoteauth/RemoteAuthFrameworkInitializer.java
new file mode 100644
index 0000000..dfd7726
--- /dev/null
+++ b/remoteauth/framework/java/android/remoteauth/RemoteAuthFrameworkInitializer.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2023 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.remoteauth;
+
+import android.annotation.SystemApi;
+import android.app.SystemServiceRegistry;
+import android.content.Context;
+
+/**
+ * Class for initializing RemoteAuth service.
+ *
+ * @hide
+ */
+@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+public final class RemoteAuthFrameworkInitializer {
+    private RemoteAuthFrameworkInitializer() {}
+
+    /**
+     * Called by {@link SystemServiceRegistry}'s static initializer and registers all Nearby
+     * services to {@link Context}, so that {@link Context#getSystemService} can return them.
+     *
+     * @throws IllegalStateException if this is called from anywhere besides {@link
+     *     SystemServiceRegistry}
+     */
+    public static void registerServiceWrappers() {
+        // TODO(b/290092977): Change to Context.REMOTE_AUTH_SERVICE after aosp/2681375
+        // is automerges from aosp-main to udc-mainline-prod
+        SystemServiceRegistry.registerContextAwareService(
+                RemoteAuthManager.REMOTE_AUTH_SERVICE,
+                RemoteAuthManager.class,
+                (context, serviceBinder) -> {
+                    IRemoteAuthService service = IRemoteAuthService.Stub.asInterface(serviceBinder);
+                    return new RemoteAuthManager(context, service);
+                });
+    }
+}
diff --git a/remoteauth/framework/java/android/remoteauth/RemoteAuthManager.java b/remoteauth/framework/java/android/remoteauth/RemoteAuthManager.java
new file mode 100644
index 0000000..c025a55
--- /dev/null
+++ b/remoteauth/framework/java/android/remoteauth/RemoteAuthManager.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright 2023 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.remoteauth;
+
+import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+import static android.remoteauth.DeviceDiscoveryCallback.STATE_LOST;
+import static android.remoteauth.DeviceDiscoveryCallback.STATE_SEEN;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.annotation.UserIdInt;
+import android.content.Context;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+
+import java.lang.ref.WeakReference;
+import java.util.Objects;
+import java.util.WeakHashMap;
+import java.util.concurrent.Executor;
+
+/**
+ * A system service providing a way to perform remote authentication-related operations such as
+ * discovering, registering and authenticating via remote authenticator.
+ *
+ * <p>To get a {@link RemoteAuthManager} instance, call the <code>
+ * Context.getSystemService(Context.REMOTE_AUTH_SERVICE)</code>.
+ *
+ * @hide
+ */
+@SystemApi(client = MODULE_LIBRARIES)
+// TODO(b/290092977): Change to Context.REMOTE_AUTH_SERVICE after aosp/2681375
+// is automerges from aosp-main to udc-mainline-prod
+@SystemService(RemoteAuthManager.REMOTE_AUTH_SERVICE)
+public class RemoteAuthManager {
+    private static final String TAG = "RemoteAuthManager";
+
+    /** @hide */
+    public static final String REMOTE_AUTH_SERVICE = "remote_auth";
+
+    private final Context mContext;
+    private final IRemoteAuthService mService;
+
+    @GuardedBy("mDiscoveryListeners")
+    private final WeakHashMap<
+                    DeviceDiscoveryCallback, WeakReference<DeviceDiscoveryListenerTransport>>
+            mDiscoveryListeners = new WeakHashMap<>();
+
+    /** @hide */
+    public RemoteAuthManager(@NonNull Context context, @NonNull IRemoteAuthService service) {
+        Objects.requireNonNull(context);
+        Objects.requireNonNull(service);
+        mContext = context;
+        mService = service;
+    }
+
+    /**
+     * Returns if this device can be enrolled in the feature.
+     *
+     * @return true if this device can be enrolled
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    // TODO(b/297301535): @RequiresPermission(MANAGE_REMOTE_AUTH)
+    public boolean isRemoteAuthSupported() {
+        try {
+            return mService.isRemoteAuthSupported();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Starts remote authenticator discovery process with timeout. Devices that are capable to
+     * operate as remote authenticators are reported via callback. The discovery stops by calling
+     * stopDiscovery or after a timeout.
+     *
+     * @param timeoutMs the duration in milliseconds after which discovery will stop automatically
+     * @param executor the callback will be executed in the executor thread
+     * @param callback to be used by the caller to get notifications about remote devices
+     * @return {@code true} if discovery began successfully, {@code false} otherwise
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    // TODO(b/297301535): @RequiresPermission(MANAGE_REMOTE_AUTH)
+    public boolean startDiscovery(
+            int timeoutMs,
+            @CallbackExecutor @NonNull Executor executor,
+            @NonNull DeviceDiscoveryCallback callback) {
+        try {
+            Preconditions.checkNotNull(callback, "invalid null callback");
+            Preconditions.checkArgument(timeoutMs > 0, "invalid timeoutMs, must be > 0");
+            Preconditions.checkNotNull(executor, "invalid null executor");
+            DeviceDiscoveryListenerTransport transport;
+            synchronized (mDiscoveryListeners) {
+                WeakReference<DeviceDiscoveryListenerTransport> reference =
+                        mDiscoveryListeners.get(callback);
+                transport = (reference != null) ? reference.get() : null;
+                if (transport == null) {
+                    transport =
+                            new DeviceDiscoveryListenerTransport(
+                                    callback, mContext.getUser().getIdentifier(), executor);
+                }
+
+                boolean result =
+                        mService.registerDiscoveryListener(
+                                transport,
+                                mContext.getUser().getIdentifier(),
+                                timeoutMs,
+                                mContext.getPackageName(),
+                                mContext.getAttributionTag());
+                if (result) {
+                    mDiscoveryListeners.put(callback, new WeakReference<>(transport));
+                    return true;
+                }
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+        return false;
+    }
+
+    /**
+     * Removes this listener from device discovery notifications. The given callback is guaranteed
+     * not to receive any invocations that happen after this method is invoked.
+     *
+     * @param callback the callback for the previously started discovery to be ended
+     * @hide
+     */
+    // Suppressed lint: Registration methods should have overload that accepts delivery Executor.
+    // Already have executor in startDiscovery() method.
+    @SuppressLint("ExecutorRegistration")
+    @SystemApi(client = MODULE_LIBRARIES)
+    // TODO(b/297301535): @RequiresPermission(MANAGE_REMOTE_AUTH)
+    public void stopDiscovery(@NonNull DeviceDiscoveryCallback callback) {
+        Preconditions.checkNotNull(callback, "invalid null scanCallback");
+        try {
+            DeviceDiscoveryListenerTransport transport;
+            synchronized (mDiscoveryListeners) {
+                WeakReference<DeviceDiscoveryListenerTransport> reference =
+                        mDiscoveryListeners.remove(callback);
+                transport = (reference != null) ? reference.get() : null;
+            }
+            if (transport != null) {
+                mService.unregisterDiscoveryListener(
+                        transport,
+                        transport.getUserId(),
+                        mContext.getPackageName(),
+                        mContext.getAttributionTag());
+            } else {
+                Log.d(
+                        TAG,
+                        "Cannot stop discovery with this callback "
+                                + "because it is not registered.");
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    private class DeviceDiscoveryListenerTransport extends IDeviceDiscoveryListener.Stub {
+
+        private volatile @NonNull DeviceDiscoveryCallback mDeviceDiscoveryCallback;
+        private Executor mExecutor;
+        private @UserIdInt int mUserId;
+
+        DeviceDiscoveryListenerTransport(
+                DeviceDiscoveryCallback deviceDiscoveryCallback,
+                @UserIdInt int userId,
+                @CallbackExecutor Executor executor) {
+            Preconditions.checkNotNull(deviceDiscoveryCallback, "invalid null callback");
+            mDeviceDiscoveryCallback = deviceDiscoveryCallback;
+            mUserId = userId;
+            mExecutor = executor;
+        }
+
+        @UserIdInt
+        int getUserId() {
+            return mUserId;
+        }
+
+        @Override
+        public void onDiscovered(RemoteDevice remoteDevice) throws RemoteException {
+            if (remoteDevice == null) {
+                Log.w(TAG, "onDiscovered is called with null device");
+                return;
+            }
+            Log.i(TAG, "Notifying the caller about discovered: " + remoteDevice);
+            mExecutor.execute(
+                    () -> {
+                        mDeviceDiscoveryCallback.onDeviceUpdate(remoteDevice, STATE_SEEN);
+                    });
+        }
+
+        @Override
+        public void onLost(RemoteDevice remoteDevice) throws RemoteException {
+            if (remoteDevice == null) {
+                Log.w(TAG, "onLost is called with null device");
+                return;
+            }
+            Log.i(TAG, "Notifying the caller about lost: " + remoteDevice);
+            mExecutor.execute(
+                    () -> {
+                        mDeviceDiscoveryCallback.onDeviceUpdate(remoteDevice, STATE_LOST);
+                    });
+        }
+
+        @Override
+        public void onTimeout() {
+            Log.i(TAG, "Notifying the caller about discovery timeout");
+            mExecutor.execute(
+                    () -> {
+                        mDeviceDiscoveryCallback.onTimeout();
+                    });
+            synchronized (mDiscoveryListeners) {
+                mDiscoveryListeners.remove(mDeviceDiscoveryCallback);
+            }
+            mDeviceDiscoveryCallback = null;
+        }
+    }
+}
diff --git a/remoteauth/framework/java/android/remoteauth/RemoteDevice.aidl b/remoteauth/framework/java/android/remoteauth/RemoteDevice.aidl
new file mode 100644
index 0000000..ea38be2
--- /dev/null
+++ b/remoteauth/framework/java/android/remoteauth/RemoteDevice.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2023 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.remoteauth;
+
+parcelable RemoteDevice;
\ No newline at end of file
diff --git a/remoteauth/framework/java/android/remoteauth/RemoteDevice.java b/remoteauth/framework/java/android/remoteauth/RemoteDevice.java
new file mode 100644
index 0000000..4cd2399
--- /dev/null
+++ b/remoteauth/framework/java/android/remoteauth/RemoteDevice.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2023 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.remoteauth;
+
+import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Remote device that can be registered as remote authenticator.
+ *
+ * @hide
+ */
+// TODO(b/295407748) Change to use @DataClass
+@SystemApi(client = MODULE_LIBRARIES)
+public final class RemoteDevice implements Parcelable {
+    /** The remote device is not registered as remote authenticator. */
+    public static final int STATE_NOT_REGISTERED = 0;
+    /** The remote device is registered as remote authenticator. */
+    public static final int STATE_REGISTERED = 1;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({STATE_NOT_REGISTERED, STATE_REGISTERED})
+    @interface RegistrationState {}
+
+    @NonNull private final String mName;
+    private final @RegistrationState int mRegistrationState;
+    private final int mConnectionId;
+
+    public static final @NonNull Creator<RemoteDevice> CREATOR =
+            new Creator<>() {
+                @Override
+                public RemoteDevice createFromParcel(Parcel in) {
+                    RemoteDevice.Builder builder = new RemoteDevice.Builder();
+                    builder.setName(in.readString());
+                    builder.setRegistrationState(in.readInt());
+                    builder.setConnectionId(in.readInt());
+
+                    return builder.build();
+                }
+
+                @Override
+                public RemoteDevice[] newArray(int size) {
+                    return new RemoteDevice[size];
+                }
+            };
+
+    private RemoteDevice(
+            @Nullable String name,
+            @RegistrationState int registrationState,
+            @NonNull int connectionId) {
+        this.mName = name;
+        this.mRegistrationState = registrationState;
+        this.mConnectionId = connectionId;
+    }
+
+    /** Gets the name of the {@link RemoteDevice} device. */
+    @Nullable
+    public String getName() {
+        return mName;
+    }
+
+    /** Returns registration state of the {@link RemoteDevice}. */
+    public @RegistrationState int getRegistrationState() {
+        return mRegistrationState;
+    }
+
+    /** Returns connection id of the {@link RemoteDevice}. */
+    @NonNull
+    public int getConnectionId() {
+        return mConnectionId;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Returns a string representation of {@link RemoteDevice}. */
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("RemoteDevice [");
+        sb.append("name=").append(mName).append(", ");
+        sb.append("registered=").append(mRegistrationState).append(", ");
+        sb.append("connectionId=").append(mConnectionId);
+        sb.append("]");
+        return sb.toString();
+    }
+
+    /** Returns true if this {@link RemoteDevice} object is equals to other. */
+    @Override
+    public boolean equals(Object other) {
+        if (other instanceof RemoteDevice) {
+            RemoteDevice otherDevice = (RemoteDevice) other;
+            return Objects.equals(this.mName, otherDevice.mName)
+                    && this.getRegistrationState() == otherDevice.getRegistrationState()
+                    && this.mConnectionId == otherDevice.mConnectionId;
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mName, mRegistrationState, mConnectionId);
+    }
+
+    /**
+     * Helper function for writing {@link RemoteDevice} to a Parcel.
+     *
+     * @param dest The Parcel in which the object should be written.
+     * @param flags Additional flags about how the object should be written.
+     */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        String name = getName();
+        dest.writeString(name);
+        dest.writeInt(getRegistrationState());
+        dest.writeInt(getConnectionId());
+    }
+
+    /** Builder for {@link RemoteDevice} objects. */
+    public static final class Builder {
+        @Nullable private String mName;
+        // represents if device is already registered
+        private @RegistrationState int mRegistrationState;
+        private int mConnectionId;
+
+        private Builder() {
+        }
+
+        public Builder(final int connectionId) {
+            this.mConnectionId = connectionId;
+        }
+
+        /**
+         * Sets the name of the {@link RemoteDevice} device.
+         *
+         * @param name of the {@link RemoteDevice}. Can be {@code null} if there is no name.
+         */
+        @NonNull
+        public RemoteDevice.Builder setName(@Nullable String name) {
+            this.mName = name;
+            return this;
+        }
+
+        /**
+         * Sets the registration state of the {@link RemoteDevice} device.
+         *
+         * @param registrationState of the {@link RemoteDevice}.
+         */
+        @NonNull
+        public RemoteDevice.Builder setRegistrationState(@RegistrationState int registrationState) {
+            this.mRegistrationState = registrationState;
+            return this;
+        }
+
+        /**
+         * Sets the connectionInfo of the {@link RemoteDevice} device.
+         *
+         * @param connectionId of the RemoteDevice.
+         */
+        @NonNull
+        public RemoteDevice.Builder setConnectionId(int connectionId) {
+            this.mConnectionId = connectionId;
+            return this;
+        }
+
+        /**
+         * Creates the {@link RemoteDevice} instance.
+         *
+         * @return the configured {@link RemoteDevice} instance.
+         */
+        @NonNull
+        public RemoteDevice build() {
+            return new RemoteDevice(mName, mRegistrationState, mConnectionId);
+        }
+    }
+}
diff --git a/remoteauth/service/Android.bp b/remoteauth/service/Android.bp
index 5c5a2fb..c3a9fb3 100644
--- a/remoteauth/service/Android.bp
+++ b/remoteauth/service/Android.bp
@@ -29,8 +29,24 @@
     defaults: [
         "framework-system-server-module-defaults"
     ],
-    libs: [],
-    static_libs: [],
+    libs: [
+        "androidx.annotation_annotation",
+        "framework-bluetooth",
+        "error_prone_annotations",
+        "framework-configinfrastructure",
+        "framework-connectivity-pre-jarjar",
+        "framework-connectivity-t-pre-jarjar",
+        "framework-statsd",
+    ],
+    static_libs: [
+        "libprotobuf-java-lite",
+        "fast-pair-lite-protos",
+        "modules-utils-build",
+        "modules-utils-handlerexecutor",
+        "modules-utils-preconditions",
+        "modules-utils-backgroundthread",
+        "presence-lite-protos",
+    ],
     sdk_version: "system_server_current",
     // This is included in service-connectivity which is 30+
     // TODO (b/293613362): allow APEXes to have service jars with higher min_sdk than the APEX
diff --git a/remoteauth/service/java/com/android/server/remoteauth/README.md b/remoteauth/service/java/com/android/server/remoteauth/README.md
index 423ab45..2f8b096 100644
--- a/remoteauth/service/java/com/android/server/remoteauth/README.md
+++ b/remoteauth/service/java/com/android/server/remoteauth/README.md
@@ -1 +1,4 @@
-This is the source root for the RemoteAuthService
\ No newline at end of file
+This is the source root for the RemoteAuthService
+
+## Remote connectivity manager
+Provides the connectivity manager to manage connections with the peer device.
diff --git a/remoteauth/service/java/com/android/server/remoteauth/RemoteAuthService.java b/remoteauth/service/java/com/android/server/remoteauth/RemoteAuthService.java
new file mode 100644
index 0000000..41ce89a
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/RemoteAuthService.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2023 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.server.remoteauth;
+
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.content.Context;
+import android.remoteauth.IDeviceDiscoveryListener;
+import android.remoteauth.IRemoteAuthService;
+
+import com.android.internal.util.Preconditions;
+
+/** Service implementing remoteauth functionality. */
+public class RemoteAuthService extends IRemoteAuthService.Stub {
+    public static final String TAG = "RemoteAuthService";
+
+    public RemoteAuthService(Context context) {
+        Preconditions.checkNotNull(context);
+        // TODO(b/290280702): Create here RemoteConnectivityManager and RangingManager
+    }
+
+    @Override
+    public boolean isRemoteAuthSupported() {
+        // TODO(b/297301535): checkPermission(mContext, MANAGE_REMOTE_AUTH);
+        // TODO(b/290676192): integrate with RangingManager
+        //  (check if UWB is supported by this device)
+        return true;
+    }
+
+    @Override
+    public boolean registerDiscoveryListener(
+            IDeviceDiscoveryListener deviceDiscoveryListener,
+            @UserIdInt int userId,
+            int timeoutMs,
+            String packageName,
+            @Nullable String attributionTag) {
+        // TODO(b/297301535): checkPermission(mContext, MANAGE_REMOTE_AUTH);
+        // TODO(b/290280702): implement register discovery logic
+        return true;
+    }
+
+    @Override
+    public void unregisterDiscoveryListener(
+            IDeviceDiscoveryListener deviceDiscoveryListener,
+            @UserIdInt int userId,
+            String packageName,
+            @Nullable String attributionTag) {
+        // TODO(b/297301535): checkPermission(mContext, MANAGE_REMOTE_AUTH);
+        // TODO(b/290094221): implement unregister logic
+    }
+
+    private static void checkPermission(Context context, String permission) {
+        context.enforceCallingOrSelfPermission(permission,
+                "Must have " + permission + " permission.");
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/CdmConnectionInfo.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/CdmConnectionInfo.java
new file mode 100644
index 0000000..8bfdd36
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/CdmConnectionInfo.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2023 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.server.remoteauth.connectivity;
+
+import android.annotation.NonNull;
+import android.annotation.TargetApi;
+import android.companion.AssociationInfo;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Encapsulates the connection information for companion device manager connections.
+ *
+ * <p>Connection information captures the details of underlying connection such as connection id,
+ * type of connection and peer device mac address.
+ */
+// TODO(b/295407748): Change to use @DataClass.
+// TODO(b/296625303): Change to VANILLA_ICE_CREAM when AssociationInfo is available in V.
+@TargetApi(Build.VERSION_CODES.TIRAMISU)
+public final class CdmConnectionInfo extends ConnectionInfo {
+    @NonNull private final AssociationInfo mAssociationInfo;
+
+    public CdmConnectionInfo(int connectionId, @NonNull AssociationInfo associationInfo) {
+       super(connectionId);
+        mAssociationInfo = associationInfo;
+    }
+
+    private CdmConnectionInfo(@NonNull Parcel in) {
+        super(in);
+        mAssociationInfo = in.readTypedObject(AssociationInfo.CREATOR);
+    }
+
+    /** Used to read CdmConnectionInfo from a Parcel */
+    @NonNull
+    public static final Parcelable.Creator<CdmConnectionInfo> CREATOR =
+            new Parcelable.Creator<CdmConnectionInfo>() {
+                public CdmConnectionInfo createFromParcel(@NonNull Parcel in) {
+                    return new CdmConnectionInfo(in);
+                }
+
+                public CdmConnectionInfo[] newArray(int size) {
+                    return new CdmConnectionInfo[size];
+                }
+            };
+
+    /** No special parcel contents. */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Flatten this CdmConnectionInfo in to a Parcel.
+     *
+     * @param out The Parcel in which the object should be written.
+     * @param flags Additional flags about how the object should be written.
+     */
+    @Override
+    public void writeToParcel(@NonNull Parcel out, int flags) {
+        super.writeToParcel(out, flags);
+        out.writeTypedObject(mAssociationInfo, 0);
+    }
+
+    public AssociationInfo getAssociationInfo() {
+        return mAssociationInfo;
+    }
+
+    /** Returns a string representation of ConnectionInfo. */
+    @Override
+    public String toString() {
+        return super.toString();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == null || !(o instanceof CdmConnectionInfo)) {
+            return false;
+        }
+        if (!super.equals(o)) {
+            return false;
+        }
+
+        CdmConnectionInfo other = (CdmConnectionInfo) o;
+        return mAssociationInfo.equals(other.getAssociationInfo());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mAssociationInfo);
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/Connection.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/Connection.java
new file mode 100644
index 0000000..eb5458d
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/Connection.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2023 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.server.remoteauth.connectivity;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A connection with the peer device.
+ *
+ * <p>Connections are used to exchange data with the peer device.
+ *
+ */
+public interface Connection {
+    /** Unknown error. */
+    int ERROR_UNKNOWN = 0;
+
+    /** Message was sent successfully. */
+    int ERROR_OK = 1;
+
+    /** Timeout occurred while waiting for response from the peer. */
+    int ERROR_DEADLINE_EXCEEDED = 2;
+
+    /** Device became unavailable while sending the message. */
+    int ERROR_DEVICE_UNAVAILABLE = 3;
+
+    /** Represents error code. */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({ERROR_UNKNOWN, ERROR_OK, ERROR_DEADLINE_EXCEEDED, ERROR_DEVICE_UNAVAILABLE})
+    @interface ErrorCode {}
+
+    /**
+     * Callback for clients to get the response of sendRequest. {@link onSuccess} is called if the
+     * peer device responds with Status::OK, otherwise runs the {@link onFailure} callback.
+     */
+    abstract class MessageResponseCallback {
+        /**
+         * Called on a success.
+         *
+         * @param buffer response from the device.
+         */
+        public void onSuccess(byte[] buffer) {}
+
+        /**
+         * Called when message sending fails.
+         *
+         * @param errorCode indicating the error.
+         */
+        public void onFailure(@ErrorCode int errorCode) {}
+    }
+
+    /**
+     * Sends a request to the peer.
+     *
+     * @param request byte array to be sent to the peer device.
+     * @param messageResponseCallback callback to be run when the peer response is received or if an
+     *     error occurred.
+     */
+    void sendRequest(byte[] request, MessageResponseCallback messageResponseCallback);
+
+    /** Triggers a disconnect from the peer device. */
+    void disconnect();
+
+    /**
+     * Returns the connection information.
+     *
+     * @return A connection information object.
+     */
+    ConnectionInfo getConnectionInfo();
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectionException.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectionException.java
new file mode 100644
index 0000000..a8c7860
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectionException.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2023 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.server.remoteauth.connectivity;
+
+import static com.android.server.remoteauth.connectivity.ConnectivityManager.ReasonCode;
+
+import android.annotation.Nullable;
+
+/** Exception that signals that the connection request failed. */
+public final class ConnectionException extends RuntimeException {
+    private final @ReasonCode int mReasonCode;
+
+    public ConnectionException(@ReasonCode int reasonCode) {
+        super();
+        this.mReasonCode = reasonCode;
+    }
+
+    public ConnectionException(@ReasonCode int reasonCode, @Nullable String message) {
+        super(message);
+        this.mReasonCode = reasonCode;
+    }
+
+    public ConnectionException(@ReasonCode int reasonCode, @Nullable Throwable cause) {
+        super(cause);
+        this.mReasonCode = reasonCode;
+    }
+
+    public ConnectionException(
+            @ReasonCode int reasonCode, @Nullable String message, @Nullable Throwable cause) {
+        super(message, cause);
+        this.mReasonCode = reasonCode;
+    }
+
+    public @ReasonCode int getReasonCode() {
+        return this.mReasonCode;
+    }
+
+    @Override
+    public String getMessage() {
+        return super.getMessage() + " Reason code: " + this.mReasonCode;
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectionInfo.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectionInfo.java
new file mode 100644
index 0000000..39bfa8d
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectionInfo.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2023 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.server.remoteauth.connectivity;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Encapsulates the connection information.
+ *
+ * <p>Connection information captures the details of underlying connection such as connection id,
+ * type of connection and peer device mac address.
+ *
+ */
+// TODO(b/295407748) Change to use @DataClass.
+public abstract class ConnectionInfo implements Parcelable {
+    int mConnectionId;
+
+    public ConnectionInfo(int connectionId) {
+        mConnectionId = connectionId;
+    }
+
+    /** Create object from Parcel */
+    public ConnectionInfo(@NonNull Parcel in) {
+        mConnectionId = in.readInt();
+    }
+
+    public int getConnectionId() {
+        return mConnectionId;
+    }
+
+    /** No special parcel contents. */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Flattens this ConnectionInfo in to a Parcel.
+     *
+     * @param out The Parcel in which the object should be written.
+     * @param flags Additional flags about how the object should be written.
+     */
+    @Override
+    public void writeToParcel(@NonNull Parcel out, int flags) {
+        out.writeInt(mConnectionId);
+    }
+
+    /** Returns string representation of ConnectionInfo. */
+    @Override
+    public String toString() {
+        return "ConnectionInfo[" + "connectionId= " + mConnectionId + "]";
+    }
+
+    /** Returns true if this ConnectionInfo object is equal to the other. */
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof ConnectionInfo)) {
+            return false;
+        }
+
+        ConnectionInfo other = (ConnectionInfo) o;
+        return mConnectionId == other.getConnectionId();
+    }
+
+    /** Returns the hashcode of this object */
+    @Override
+    public int hashCode() {
+        return Objects.hash(mConnectionId);
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectivityManager.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectivityManager.java
new file mode 100644
index 0000000..bc0d77e
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectivityManager.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2023 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.server.remoteauth.connectivity;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Performs discovery and triggers a connection to an associated device.
+ */
+public interface ConnectivityManager {
+    /**
+     * Starts device discovery.
+     *
+     * <p>Discovery continues until stopped using {@link stopDiscovery} or times out.
+     *
+     * @param discoveryFilter to filter for devices during discovery.
+     * @param discoveredDeviceReceiver callback to run when device is found or lost.
+     */
+    void startDiscovery(
+            @NonNull DiscoveryFilter discoveryFilter,
+            @NonNull DiscoveredDeviceReceiver discoveredDeviceReceiver);
+
+    /**
+     * Stops device discovery.
+     *
+     * @param discoveryFilter filter used to start discovery.
+     * @param discoveredDeviceReceiver callback passed with startDiscovery.
+     */
+    void stopDiscovery(
+            @NonNull DiscoveryFilter discoveryFilter,
+            @NonNull DiscoveredDeviceReceiver discoveredDeviceReceiver);
+
+    /** Unknown reason for connection failure. */
+    int ERROR_REASON_UNKNOWN = 0;
+
+    /** Indicates that the connection request timed out. */
+    int ERROR_CONNECTION_TIMED_OUT = 1;
+
+    /** Indicates that the connection request was refused by the peer. */
+    int ERROR_CONNECTION_REFUSED = 2;
+
+    /** Indicates that the peer device was unavailable. */
+    int ERROR_DEVICE_UNAVAILABLE = 3;
+
+    /** Reason codes for connect failure. */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({ERROR_REASON_UNKNOWN, ERROR_CONNECTION_TIMED_OUT, ERROR_CONNECTION_REFUSED,
+            ERROR_DEVICE_UNAVAILABLE})
+    @interface ReasonCode {}
+
+    /**
+     * Initiates a connection with the peer device.
+     *
+     * @param connectionInfo of the device discovered using {@link startDiscovery}.
+     * @param eventListener to listen for events from the underlying transport.
+     * @return {@link Connection} object or null connection is not established.
+     * @throws ConnectionException in case connection cannot be established.
+     */
+    @Nullable
+    Connection connect(
+            @NonNull ConnectionInfo connectionInfo, @NonNull EventListener eventListener);
+
+    /**
+     * Message received callback.
+     *
+     * <p>Clients should implement this callback to receive messages from the peer device.
+     */
+    abstract class MessageReceiver {
+        /**
+         * Receive message from the peer device.
+         *
+         * <p>Clients can set empty buffer as an ACK to the request.
+         *
+         * @param messageIn message from peer device.
+         * @param responseCallback {@link ResponseCallback} callback to send the response back.
+         */
+        public void onMessageReceived(byte[] messageIn, ResponseCallback responseCallback) {}
+    }
+
+    /**
+     * Starts listening for incoming messages.
+     *
+     * <p>Runs MessageReceiver callback when a message is received.
+     *
+     * @param messageReceiver to receive messages.
+     * @throws AssertionError if a listener is already configured.
+     */
+    void startListening(MessageReceiver messageReceiver);
+
+    /**
+     * Stops listening to incoming messages.
+     *
+     * @param messageReceiver to receive messages.
+     */
+    void stopListening(MessageReceiver messageReceiver);
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveredDevice.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveredDevice.java
new file mode 100644
index 0000000..a3e1e58
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveredDevice.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2023 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.server.remoteauth.connectivity;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.util.Objects;
+
+/** Device discovered on a network interface like Bluetooth. */
+public final class DiscoveredDevice {
+    private @NonNull ConnectionInfo mConnectionInfo;
+    private @Nullable String mDisplayName;
+
+    public DiscoveredDevice(
+            @NonNull ConnectionInfo connectionInfo, @Nullable String displayName) {
+        this.mConnectionInfo = connectionInfo;
+        this.mDisplayName = displayName;
+    }
+
+    /**
+     * Returns connection information.
+     *
+     * @return connection information.
+     */
+    @NonNull
+    public ConnectionInfo getConnectionInfo() {
+        return this.mConnectionInfo;
+    }
+
+    /**
+     * Returns display name of the device.
+     *
+     * @return display name string.
+     */
+    @Nullable
+    public String getDisplayName() {
+        return this.mDisplayName;
+    }
+
+    /**
+     * Checks for equality between this and other object.
+     *
+     * @return true if equal, false otherwise.
+     */
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof DiscoveredDevice)) {
+            return false;
+        }
+
+        DiscoveredDevice other = (DiscoveredDevice) o;
+        return mConnectionInfo.equals(other.getConnectionInfo())
+                && mDisplayName.equals(other.getDisplayName());
+    }
+
+    /**
+     * Returns hash code of the object.
+     *
+     * @return hash code.
+     */
+    @Override
+    public int hashCode() {
+        return Objects.hash(mDisplayName, mConnectionInfo.getConnectionId());
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveredDeviceReceiver.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveredDeviceReceiver.java
new file mode 100644
index 0000000..90a3e30
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveredDeviceReceiver.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 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.server.remoteauth.connectivity;
+
+/** Callbacks triggered on discovery. */
+public abstract class DiscoveredDeviceReceiver {
+    /**
+     * Callback called when a device matching the discovery filter is found.
+     *
+     * @param discoveredDevice the discovered device.
+     */
+    public void onDiscovered(DiscoveredDevice discoveredDevice) {}
+
+    /**
+     * Callback called when a previously discovered device using {@link
+     * ConnectivityManager#startDiscovery} is lost.
+     *
+     * @param discoveredDevice the lost device
+     */
+    public void onLost(DiscoveredDevice discoveredDevice) {}
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveryFilter.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveryFilter.java
new file mode 100644
index 0000000..36c4b60
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveryFilter.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2023 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.server.remoteauth.connectivity;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Filter for device discovery.
+ *
+ * <p>Callers can use this class to provide a discovery filter to the {@link
+ * ConnectivityManager.startDiscovery} method. A device is discovered if it matches at least one of
+ * the filter criteria (device type, name or peer address).
+ */
+public final class DiscoveryFilter {
+
+    /** Device type WATCH. */
+    public static final int WATCH = 0;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({WATCH})
+    public @interface DeviceType {}
+
+    private @DeviceType int mDeviceType;
+    private final @Nullable String mDeviceName;
+    private final @Nullable String mPeerAddress;
+
+    public DiscoveryFilter(
+            @DeviceType int deviceType, @Nullable String deviceName, @Nullable String peerAddress) {
+        this.mDeviceType = deviceType;
+        this.mDeviceName = deviceName;
+        this.mPeerAddress = peerAddress;
+    }
+
+    /**
+     * Returns device type.
+     *
+     * @return device type.
+     */
+    public @DeviceType int getDeviceType() {
+        return this.mDeviceType;
+    }
+
+    /**
+     * Returns device name.
+     *
+     * @return device name.
+     */
+    public @Nullable String getDeviceName() {
+        return this.mDeviceName;
+    }
+
+    /**
+     * Returns mac address of the peer device .
+     *
+     * @return mac address string.
+     */
+    public @Nullable String getPeerAddress() {
+        return this.mPeerAddress;
+    }
+
+    /** Builder for {@link DiscoverFilter} */
+    public static class Builder {
+        private @DeviceType int mDeviceType;
+        private @Nullable String mDeviceName;
+        private @Nullable String mPeerAddress;
+
+        private Builder() {}
+
+        /** Static method to create a new builder */
+        public static Builder newInstance() {
+            return new Builder();
+        }
+
+        /**
+         * Sets the device type of the DiscoveryFilter.
+         *
+         * @param deviceType of the peer device.
+         */
+        @NonNull
+        public Builder setDeviceType(@DeviceType int deviceType) {
+            mDeviceType = deviceType;
+            return this;
+        }
+
+        /**
+         * Sets the device name.
+         *
+         * @param deviceName May be null.
+         */
+        @NonNull
+        public Builder setDeviceName(String deviceName) {
+            mDeviceName = deviceName;
+            return this;
+        }
+
+        /**
+         * Sets the peer address.
+         *
+         * @param peerAddress Mac address of the peer device.
+         */
+        @NonNull
+        public Builder setPeerAddress(String peerAddress) {
+            mPeerAddress = peerAddress;
+            return this;
+        }
+
+        /** Builds the DiscoveryFilter object. */
+        @NonNull
+        public DiscoveryFilter build() {
+            return new DiscoveryFilter(this.mDeviceType, this.mDeviceName, this.mPeerAddress);
+        }
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/EventListener.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/EventListener.java
new file mode 100644
index 0000000..d07adb1
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/EventListener.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2023 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.server.remoteauth.connectivity;
+
+import android.annotation.NonNull;
+
+/**
+ * Listens to the events from underlying transport.
+ */
+interface EventListener {
+    /** Called when remote device is disconnected from the underlying transport. */
+    void onDisconnect(@NonNull ConnectionInfo connectionInfo);
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/ResponseCallback.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/ResponseCallback.java
new file mode 100644
index 0000000..8a09ab3
--- /dev/null
+++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/ResponseCallback.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2023 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.server.remoteauth.connectivity;
+
+import android.annotation.NonNull;
+
+/**
+ * Abstract class to expose a callback for clients to send a response to the peer device.
+ *
+ * <p>When a device receives a message on a connection, this object is constructed using the
+ * connection information of the connection and the message id from the incoming message. This
+ * object is forwarded to the clients of the connection to allow them to send a response to the peer
+ * device.
+ */
+public abstract class ResponseCallback {
+    private final long mMessageId;
+    private final ConnectionInfo mConnectionInfo;
+
+    public ResponseCallback(long messageId, @NonNull ConnectionInfo connectionInfo) {
+        mMessageId = messageId;
+        mConnectionInfo = connectionInfo;
+    }
+
+    /**
+     * Returns message id identifying the message.
+     *
+     * @return message id of this message.
+     */
+    public long getMessageId() {
+        return mMessageId;
+    }
+
+    /**
+     * Returns connection info from the response.
+     *
+     * @return connection info.
+     */
+    @NonNull
+    public ConnectionInfo getConnectionInfo() {
+        return mConnectionInfo;
+    }
+
+    /**
+     * Callback to send a response to the peer device.
+     *
+     * @param response buffer to send to the peer device.
+     */
+    public void onResponse(@NonNull byte[] response) {}
+}
diff --git a/remoteauth/tests/unit/Android.bp b/remoteauth/tests/unit/Android.bp
index 8c08a1b..4b92d84 100644
--- a/remoteauth/tests/unit/Android.bp
+++ b/remoteauth/tests/unit/Android.bp
@@ -37,6 +37,7 @@
         "androidx.test.rules",
         "framework-remoteauth-static",
         "junit",
+        "libprotobuf-java-lite",
         "platform-test-annotations",
         "service-remoteauth-pre-jarjar",
         "truth-prebuilt",
diff --git a/remoteauth/tests/unit/src/android/remoteauth/RemoteAuthManagerTest.java b/remoteauth/tests/unit/src/android/remoteauth/RemoteAuthManagerTest.java
index 5cf3e6b..6b43355 100644
--- a/remoteauth/tests/unit/src/android/remoteauth/RemoteAuthManagerTest.java
+++ b/remoteauth/tests/unit/src/android/remoteauth/RemoteAuthManagerTest.java
@@ -23,6 +23,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -31,6 +32,9 @@
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class RemoteAuthManagerTest {
+    @Before
+    public void setUp() {}
+
     @Test
     public void testStub() {
         assertTrue(true);
diff --git a/service-t/Android.bp b/service-t/Android.bp
index c277cf6..83caf35 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -55,6 +55,8 @@
         "framework-wifi",
         "service-connectivity-pre-jarjar",
         "service-nearby-pre-jarjar",
+        "service-thread-pre-jarjar",
+        "service-remoteauth-pre-jarjar",
         "ServiceConnectivityResources",
         "unsupportedappusage",
     ],
@@ -119,4 +121,4 @@
     visibility: [
         "//visibility:private",
     ],
-}
\ No newline at end of file
+}
diff --git a/service-t/jni/com_android_server_net_NetworkStatsService.cpp b/service-t/jni/com_android_server_net_NetworkStatsService.cpp
index 8d8dc32..bdbb655 100644
--- a/service-t/jni/com_android_server_net_NetworkStatsService.cpp
+++ b/service-t/jni/com_android_server_net_NetworkStatsService.cpp
@@ -47,11 +47,9 @@
     RX_PACKETS = 1,
     TX_BYTES = 2,
     TX_PACKETS = 3,
-    TCP_RX_PACKETS = 4,
-    TCP_TX_PACKETS = 5
 };
 
-static uint64_t getStatsType(Stats* stats, StatsType type) {
+static uint64_t getStatsType(StatsValue* stats, StatsType type) {
     switch (type) {
         case RX_BYTES:
             return stats->rxBytes;
@@ -61,17 +59,13 @@
             return stats->txBytes;
         case TX_PACKETS:
             return stats->txPackets;
-        case TCP_RX_PACKETS:
-            return stats->tcpRxPackets;
-        case TCP_TX_PACKETS:
-            return stats->tcpTxPackets;
         default:
             return UNKNOWN;
     }
 }
 
 static jlong nativeGetTotalStat(JNIEnv* env, jclass clazz, jint type) {
-    Stats stats = {};
+    StatsValue stats = {};
 
     if (bpfGetIfaceStats(NULL, &stats) == 0) {
         return getStatsType(&stats, (StatsType) type);
@@ -86,7 +80,7 @@
         return UNKNOWN;
     }
 
-    Stats stats = {};
+    StatsValue stats = {};
 
     if (bpfGetIfaceStats(iface8.c_str(), &stats) == 0) {
         return getStatsType(&stats, (StatsType) type);
@@ -96,7 +90,7 @@
 }
 
 static jlong nativeGetIfIndexStat(JNIEnv* env, jclass clazz, jint ifindex, jint type) {
-    Stats stats = {};
+    StatsValue stats = {};
     if (bpfGetIfIndexStats(ifindex, &stats) == 0) {
         return getStatsType(&stats, (StatsType) type);
     } else {
@@ -105,7 +99,7 @@
 }
 
 static jlong nativeGetUidStat(JNIEnv* env, jclass clazz, jint uid, jint type) {
-    Stats stats = {};
+    StatsValue stats = {};
 
     if (bpfGetUidStats(uid, &stats) == 0) {
         return getStatsType(&stats, (StatsType) type);
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
index 2e6e3e5..fed2979 100644
--- a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
@@ -40,39 +40,26 @@
 
 using base::Result;
 
-// This explicitly zero-initializes the relevant Stats fields.
-void InitStats(Stats* stats) {
-    stats->rxBytes = 0;
-    stats->rxPackets = 0;
-    stats->txBytes = 0;
-    stats->txPackets = 0;
-    stats->tcpRxPackets = -1;
-    stats->tcpTxPackets = -1;
-}
-
-int bpfGetUidStatsInternal(uid_t uid, Stats* stats,
+int bpfGetUidStatsInternal(uid_t uid, StatsValue* stats,
                            const BpfMap<uint32_t, StatsValue>& appUidStatsMap) {
-    InitStats(stats);
     auto statsEntry = appUidStatsMap.readValue(uid);
-    if (statsEntry.ok()) {
-        stats->rxPackets = statsEntry.value().rxPackets;
-        stats->txPackets = statsEntry.value().txPackets;
-        stats->rxBytes = statsEntry.value().rxBytes;
-        stats->txBytes = statsEntry.value().txBytes;
+    if (!statsEntry.ok()) {
+        *stats = {};
+        return (statsEntry.error().code() == ENOENT) ? 0 : -statsEntry.error().code();
     }
-    return (statsEntry.ok() || statsEntry.error().code() == ENOENT) ? 0
-                                                                    : -statsEntry.error().code();
+    *stats = statsEntry.value();
+    return 0;
 }
 
-int bpfGetUidStats(uid_t uid, Stats* stats) {
+int bpfGetUidStats(uid_t uid, StatsValue* stats) {
     static BpfMapRO<uint32_t, StatsValue> appUidStatsMap(APP_UID_STATS_MAP_PATH);
     return bpfGetUidStatsInternal(uid, stats, appUidStatsMap);
 }
 
-int bpfGetIfaceStatsInternal(const char* iface, Stats* stats,
+int bpfGetIfaceStatsInternal(const char* iface, StatsValue* stats,
                              const BpfMap<uint32_t, StatsValue>& ifaceStatsMap,
                              const BpfMap<uint32_t, IfaceValue>& ifaceNameMap) {
-    InitStats(stats);
+    *stats = {};
     int64_t unknownIfaceBytesTotal = 0;
     const auto processIfaceStats =
             [iface, stats, &ifaceNameMap, &unknownIfaceBytesTotal](
@@ -88,10 +75,7 @@
             if (!statsEntry.ok()) {
                 return statsEntry.error();
             }
-            stats->rxPackets += statsEntry.value().rxPackets;
-            stats->txPackets += statsEntry.value().txPackets;
-            stats->rxBytes += statsEntry.value().rxBytes;
-            stats->txBytes += statsEntry.value().txBytes;
+            *stats += statsEntry.value();
         }
         return Result<void>();
     };
@@ -99,27 +83,24 @@
     return res.ok() ? 0 : -res.error().code();
 }
 
-int bpfGetIfaceStats(const char* iface, Stats* stats) {
+int bpfGetIfaceStats(const char* iface, StatsValue* stats) {
     static BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
     static BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
     return bpfGetIfaceStatsInternal(iface, stats, ifaceStatsMap, ifaceIndexNameMap);
 }
 
-int bpfGetIfIndexStatsInternal(uint32_t ifindex, Stats* stats,
+int bpfGetIfIndexStatsInternal(uint32_t ifindex, StatsValue* stats,
                                const BpfMap<uint32_t, StatsValue>& ifaceStatsMap) {
-    InitStats(stats);
     auto statsEntry = ifaceStatsMap.readValue(ifindex);
-    if (statsEntry.ok()) {
-        stats->rxPackets = statsEntry.value().rxPackets;
-        stats->txPackets = statsEntry.value().txPackets;
-        stats->rxBytes = statsEntry.value().rxBytes;
-        stats->txBytes = statsEntry.value().txBytes;
-        return 0;
+    if (!statsEntry.ok()) {
+        *stats = {};
+        return (statsEntry.error().code() == ENOENT) ? 0 : -statsEntry.error().code();
     }
-    return (statsEntry.error().code() == ENOENT) ? 0 : -statsEntry.error().code();
+    *stats = statsEntry.value();
+    return 0;
 }
 
-int bpfGetIfIndexStats(int ifindex, Stats* stats) {
+int bpfGetIfIndexStats(int ifindex, StatsValue* stats) {
     static BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
     return bpfGetIfIndexStatsInternal(ifindex, stats, ifaceStatsMap);
 }
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
index 4f85d9b..76c56eb 100644
--- a/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
@@ -116,7 +116,7 @@
         EXPECT_RESULT_OK(mFakeIfaceIndexNameMap.writeValue(ifaceIndex, iface, BPF_ANY));
     }
 
-    void expectStatsEqual(const StatsValue& target, const Stats& result) {
+    void expectStatsEqual(const StatsValue& target, const StatsValue& result) {
         EXPECT_EQ(target.rxPackets, result.rxPackets);
         EXPECT_EQ(target.rxBytes, result.rxBytes);
         EXPECT_EQ(target.txPackets, result.txPackets);
@@ -194,7 +194,7 @@
             .txPackets = 0,
             .txBytes = 0,
     };
-    Stats result1 = {};
+    StatsValue result1 = {};
     ASSERT_EQ(0, bpfGetUidStatsInternal(TEST_UID1, &result1, mFakeAppUidStatsMap));
     expectStatsEqual(value1, result1);
 }
@@ -217,11 +217,11 @@
     };
     ASSERT_RESULT_OK(mFakeAppUidStatsMap.writeValue(TEST_UID1, value1, BPF_ANY));
     ASSERT_RESULT_OK(mFakeAppUidStatsMap.writeValue(TEST_UID2, value2, BPF_ANY));
-    Stats result1 = {};
+    StatsValue result1 = {};
     ASSERT_EQ(0, bpfGetUidStatsInternal(TEST_UID1, &result1, mFakeAppUidStatsMap));
     expectStatsEqual(value1, result1);
 
-    Stats result2 = {};
+    StatsValue result2 = {};
     ASSERT_EQ(0, bpfGetUidStatsInternal(TEST_UID2, &result2, mFakeAppUidStatsMap));
     expectStatsEqual(value2, result2);
     std::vector<stats_line> lines;
@@ -255,15 +255,15 @@
     ifaceStatsKey = IFACE_INDEX3;
     EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
 
-    Stats result1 = {};
+    StatsValue result1 = {};
     ASSERT_EQ(0, bpfGetIfaceStatsInternal(IFACE_NAME1, &result1, mFakeIfaceStatsMap,
                                           mFakeIfaceIndexNameMap));
     expectStatsEqual(value1, result1);
-    Stats result2 = {};
+    StatsValue result2 = {};
     ASSERT_EQ(0, bpfGetIfaceStatsInternal(IFACE_NAME2, &result2, mFakeIfaceStatsMap,
                                           mFakeIfaceIndexNameMap));
     expectStatsEqual(value2, result2);
-    Stats totalResult = {};
+    StatsValue totalResult = {};
     ASSERT_EQ(0, bpfGetIfaceStatsInternal(NULL, &totalResult, mFakeIfaceStatsMap,
                                           mFakeIfaceIndexNameMap));
     StatsValue totalValue = {
@@ -284,7 +284,7 @@
     };
     EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(IFACE_INDEX1, value, BPF_ANY));
 
-    Stats result = {};
+    StatsValue result = {};
     ASSERT_EQ(0, bpfGetIfIndexStatsInternal(IFACE_INDEX1, &result, mFakeIfaceStatsMap));
     expectStatsEqual(value, result);
 }
diff --git a/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h b/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h
index 0a9c012..ea068fc 100644
--- a/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h
+++ b/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h
@@ -56,14 +56,14 @@
 bool operator<(const stats_line& lhs, const stats_line& rhs);
 
 // For test only
-int bpfGetUidStatsInternal(uid_t uid, Stats* stats,
+int bpfGetUidStatsInternal(uid_t uid, StatsValue* stats,
                            const BpfMap<uint32_t, StatsValue>& appUidStatsMap);
 // For test only
-int bpfGetIfaceStatsInternal(const char* iface, Stats* stats,
+int bpfGetIfaceStatsInternal(const char* iface, StatsValue* stats,
                              const BpfMap<uint32_t, StatsValue>& ifaceStatsMap,
                              const BpfMap<uint32_t, IfaceValue>& ifaceNameMap);
 // For test only
-int bpfGetIfIndexStatsInternal(uint32_t ifindex, Stats* stats,
+int bpfGetIfIndexStatsInternal(uint32_t ifindex, StatsValue* stats,
                                const BpfMap<uint32_t, StatsValue>& ifaceStatsMap);
 // For test only
 int parseBpfNetworkStatsDetailInternal(std::vector<stats_line>& lines,
@@ -113,9 +113,9 @@
                                     const BpfMap<uint32_t, StatsValue>& statsMap,
                                     const BpfMap<uint32_t, IfaceValue>& ifaceMap);
 
-int bpfGetUidStats(uid_t uid, Stats* stats);
-int bpfGetIfaceStats(const char* iface, Stats* stats);
-int bpfGetIfIndexStats(int ifindex, Stats* stats);
+int bpfGetUidStats(uid_t uid, StatsValue* stats);
+int bpfGetIfaceStats(const char* iface, StatsValue* stats);
+int bpfGetIfIndexStats(int ifindex, StatsValue* stats);
 int parseBpfNetworkStatsDetail(std::vector<stats_line>* lines);
 
 int parseBpfNetworkStatsDev(std::vector<stats_line>* lines);
diff --git a/service-t/src/com/android/server/ConnectivityServiceInitializer.java b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
index 626c2eb..2da067a 100644
--- a/service-t/src/com/android/server/ConnectivityServiceInitializer.java
+++ b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
@@ -17,6 +17,7 @@
 package com.android.server;
 
 import android.content.Context;
+import android.remoteauth.RemoteAuthManager;
 import android.util.Log;
 
 import com.android.modules.utils.build.SdkLevel;
@@ -25,6 +26,7 @@
 import com.android.server.ethernet.EthernetService;
 import com.android.server.ethernet.EthernetServiceImpl;
 import com.android.server.nearby.NearbyService;
+import com.android.server.remoteauth.RemoteAuthService;
 
 /**
  * Connectivity service initializer for core networking. This is called by system server to create
@@ -38,6 +40,7 @@
     private final NsdService mNsdService;
     private final NearbyService mNearbyService;
     private final EthernetServiceImpl mEthernetServiceImpl;
+    private final RemoteAuthService mRemoteAuthService;
 
     public ConnectivityServiceInitializer(Context context) {
         super(context);
@@ -49,6 +52,7 @@
         mConnectivityNative = createConnectivityNativeService(context);
         mNsdService = createNsdService(context);
         mNearbyService = createNearbyService(context);
+        mRemoteAuthService = createRemoteAuthService(context);
     }
 
     @Override
@@ -85,6 +89,11 @@
                     /* allowIsolated= */ false);
         }
 
+        if (mRemoteAuthService != null) {
+            Log.i(TAG, "Registering " + RemoteAuthManager.REMOTE_AUTH_SERVICE);
+            publishBinderService(RemoteAuthManager.REMOTE_AUTH_SERVICE, mRemoteAuthService,
+                    /* allowIsolated= */ false);
+        }
     }
 
     @Override
@@ -140,6 +149,20 @@
         }
     }
 
+    /** Return RemoteAuth service instance */
+    private RemoteAuthService createRemoteAuthService(final Context context) {
+        if (!SdkLevel.isAtLeastV()) return null;
+        try {
+            return new RemoteAuthService(context);
+        } catch (UnsupportedOperationException e) {
+            // RemoteAuth is not yet supported in all branches
+            // TODO: remove catch clause when it is available.
+            Log.i(TAG, "Skipping unsupported service "
+                    + RemoteAuthManager.REMOTE_AUTH_SERVICE);
+            return null;
+        }
+    }
+
     /**
      * Return EthernetServiceImpl instance or null if current SDK is lower than T or Ethernet
      * service isn't necessary.
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index ff1f02d..6485e99 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -144,7 +144,7 @@
      * "mdns_advertiser_allowlist_othertype_version"
      * would be used to toggle MdnsDiscoveryManager / MdnsAdvertiser for each type. The flags will
      * be read with
-     * {@link DeviceConfigUtils#isFeatureEnabled(Context, String, String, String, boolean)}.
+     * {@link DeviceConfigUtils#isTetheringFeatureEnabled}
      *
      * @see #MDNS_DISCOVERY_MANAGER_ALLOWLIST_FLAG_PREFIX
      * @see #MDNS_ADVERTISER_ALLOWLIST_FLAG_PREFIX
@@ -1700,9 +1700,9 @@
          * @return true if the MdnsDiscoveryManager feature is enabled.
          */
         public boolean isMdnsDiscoveryManagerEnabled(Context context) {
-            return isAtLeastU() || DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_TETHERING,
-                    MDNS_DISCOVERY_MANAGER_VERSION, DeviceConfigUtils.TETHERING_MODULE_NAME,
-                    false /* defaultEnabled */);
+            return isAtLeastU() || DeviceConfigUtils.isTetheringFeatureEnabled(context,
+                    NAMESPACE_TETHERING, MDNS_DISCOVERY_MANAGER_VERSION,
+                    DeviceConfigUtils.TETHERING_MODULE_NAME, false /* defaultEnabled */);
         }
 
         /**
@@ -1712,9 +1712,9 @@
          * @return true if the MdnsAdvertiser feature is enabled.
          */
         public boolean isMdnsAdvertiserEnabled(Context context) {
-            return isAtLeastU() || DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_TETHERING,
-                    MDNS_ADVERTISER_VERSION, DeviceConfigUtils.TETHERING_MODULE_NAME,
-                    false /* defaultEnabled */);
+            return isAtLeastU() || DeviceConfigUtils.isTetheringFeatureEnabled(context,
+                    NAMESPACE_TETHERING, MDNS_ADVERTISER_VERSION,
+                    DeviceConfigUtils.TETHERING_MODULE_NAME, false /* defaultEnabled */);
         }
 
         /**
@@ -1728,10 +1728,10 @@
         }
 
         /**
-         * @see DeviceConfigUtils#isFeatureEnabled(Context, String, String, String, boolean)
+         * @see DeviceConfigUtils#isTetheringFeatureEnabled
          */
         public boolean isFeatureEnabled(Context context, String feature) {
-            return DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_TETHERING,
+            return DeviceConfigUtils.isTetheringFeatureEnabled(context, NAMESPACE_TETHERING,
                     feature, DeviceConfigUtils.TETHERING_MODULE_NAME, false /* defaultEnabled */);
         }
 
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
index 097dbe0..2ef7368 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
@@ -34,7 +34,6 @@
 import java.net.Inet6Address;
 import java.net.InetSocketAddress;
 import java.util.List;
-import java.util.Objects;
 
 /**
  * The {@link MdnsMultinetworkSocketClient} manages the multinetwork socket for mDns
@@ -49,10 +48,9 @@
     @NonNull private final MdnsSocketProvider mSocketProvider;
     @NonNull private final SharedLog mSharedLog;
 
-    private final ArrayMap<MdnsServiceBrowserListener, InterfaceSocketCallback> mRequestedNetworks =
+    private final ArrayMap<MdnsServiceBrowserListener, InterfaceSocketCallback> mSocketRequests =
             new ArrayMap<>();
-    private final ArrayMap<MdnsInterfaceSocket, ReadPacketHandler> mSocketPacketHandlers =
-            new ArrayMap<>();
+    private final ArrayMap<SocketKey, ReadPacketHandler> mSocketPacketHandlers = new ArrayMap<>();
     private MdnsSocketClientBase.Callback mCallback = null;
     private int mReceivedPacketNumber = 0;
 
@@ -68,8 +66,7 @@
         @NonNull
         private final SocketCreationCallback mSocketCreationCallback;
         @NonNull
-        private final ArrayMap<MdnsInterfaceSocket, SocketKey> mActiveSockets =
-                new ArrayMap<>();
+        private final ArrayMap<SocketKey, MdnsInterfaceSocket> mActiveSockets = new ArrayMap<>();
 
         InterfaceSocketCallback(SocketCreationCallback socketCreationCallback) {
             mSocketCreationCallback = socketCreationCallback;
@@ -80,27 +77,27 @@
                 @NonNull MdnsInterfaceSocket socket, @NonNull List<LinkAddress> addresses) {
             // The socket may be already created by other request before, try to get the stored
             // ReadPacketHandler.
-            ReadPacketHandler handler = mSocketPacketHandlers.get(socket);
+            ReadPacketHandler handler = mSocketPacketHandlers.get(socketKey);
             if (handler == null) {
                 // First request to create this socket. Initial a ReadPacketHandler for this socket.
                 handler = new ReadPacketHandler(socketKey);
-                mSocketPacketHandlers.put(socket, handler);
+                mSocketPacketHandlers.put(socketKey, handler);
             }
             socket.addPacketHandler(handler);
-            mActiveSockets.put(socket, socketKey);
+            mActiveSockets.put(socketKey, socket);
             mSocketCreationCallback.onSocketCreated(socketKey);
         }
 
         @Override
         public void onInterfaceDestroyed(@NonNull SocketKey socketKey,
                 @NonNull MdnsInterfaceSocket socket) {
-            notifySocketDestroyed(socket);
-            maybeCleanupPacketHandler(socket);
+            notifySocketDestroyed(socketKey);
+            maybeCleanupPacketHandler(socketKey);
         }
 
-        private void notifySocketDestroyed(@NonNull MdnsInterfaceSocket socket) {
-            final SocketKey socketKey = mActiveSockets.remove(socket);
-            if (!isSocketActive(socket)) {
+        private void notifySocketDestroyed(@NonNull SocketKey socketKey) {
+            mActiveSockets.remove(socketKey);
+            if (!isSocketActive(socketKey)) {
                 mSocketCreationCallback.onSocketDestroyed(socketKey);
             }
         }
@@ -108,35 +105,38 @@
         void onNetworkUnrequested() {
             for (int i = mActiveSockets.size() - 1; i >= 0; i--) {
                 // Iterate from the end so the socket can be removed
-                final MdnsInterfaceSocket socket = mActiveSockets.keyAt(i);
-                notifySocketDestroyed(socket);
-                maybeCleanupPacketHandler(socket);
+                final SocketKey socketKey = mActiveSockets.keyAt(i);
+                notifySocketDestroyed(socketKey);
+                maybeCleanupPacketHandler(socketKey);
             }
         }
     }
 
-    private boolean isSocketActive(@NonNull MdnsInterfaceSocket socket) {
-        for (int i = 0; i < mRequestedNetworks.size(); i++) {
-            final InterfaceSocketCallback isc = mRequestedNetworks.valueAt(i);
-            if (isc.mActiveSockets.containsKey(socket)) {
+    private boolean isSocketActive(@NonNull SocketKey socketKey) {
+        for (int i = 0; i < mSocketRequests.size(); i++) {
+            final InterfaceSocketCallback ifaceSocketCallback = mSocketRequests.valueAt(i);
+            if (ifaceSocketCallback.mActiveSockets.containsKey(socketKey)) {
                 return true;
             }
         }
         return false;
     }
 
-    private ArrayMap<MdnsInterfaceSocket, SocketKey> getActiveSockets() {
-        final ArrayMap<MdnsInterfaceSocket, SocketKey> sockets = new ArrayMap<>();
-        for (int i = 0; i < mRequestedNetworks.size(); i++) {
-            final InterfaceSocketCallback isc = mRequestedNetworks.valueAt(i);
-            sockets.putAll(isc.mActiveSockets);
+    @Nullable
+    private MdnsInterfaceSocket getTargetSocket(@NonNull SocketKey targetSocketKey) {
+        for (int i = 0; i < mSocketRequests.size(); i++) {
+            final InterfaceSocketCallback ifaceSocketCallback = mSocketRequests.valueAt(i);
+            final int index = ifaceSocketCallback.mActiveSockets.indexOfKey(targetSocketKey);
+            if (index >= 0) {
+                return ifaceSocketCallback.mActiveSockets.valueAt(index);
+            }
         }
-        return sockets;
+        return null;
     }
 
-    private void maybeCleanupPacketHandler(@NonNull MdnsInterfaceSocket socket) {
-        if (isSocketActive(socket)) return;
-        mSocketPacketHandlers.remove(socket);
+    private void maybeCleanupPacketHandler(@NonNull SocketKey socketKey) {
+        if (isSocketActive(socketKey)) return;
+        mSocketPacketHandlers.remove(socketKey);
     }
 
     private class ReadPacketHandler implements MulticastPacketReader.PacketHandler {
@@ -171,14 +171,14 @@
     public void notifyNetworkRequested(@NonNull MdnsServiceBrowserListener listener,
             @Nullable Network network, @NonNull SocketCreationCallback socketCreationCallback) {
         ensureRunningOnHandlerThread(mHandler);
-        InterfaceSocketCallback callback = mRequestedNetworks.get(listener);
+        InterfaceSocketCallback callback = mSocketRequests.get(listener);
         if (callback != null) {
             throw new IllegalArgumentException("Can not register duplicated listener");
         }
 
         if (DBG) mSharedLog.v("notifyNetworkRequested: network=" + network);
         callback = new InterfaceSocketCallback(socketCreationCallback);
-        mRequestedNetworks.put(listener, callback);
+        mSocketRequests.put(listener, callback);
         mSocketProvider.requestSocket(network, callback);
     }
 
@@ -186,14 +186,14 @@
     @Override
     public void notifyNetworkUnrequested(@NonNull MdnsServiceBrowserListener listener) {
         ensureRunningOnHandlerThread(mHandler);
-        final InterfaceSocketCallback callback = mRequestedNetworks.get(listener);
+        final InterfaceSocketCallback callback = mSocketRequests.get(listener);
         if (callback == null) {
             mSharedLog.e("Can not be unrequested with unknown listener=" + listener);
             return;
         }
         callback.onNetworkUnrequested();
-        // onNetworkUnrequested does cleanups based on mRequestedNetworks, only remove afterwards
-        mRequestedNetworks.remove(listener);
+        // onNetworkUnrequested does cleanups based on mSocketRequests, only remove afterwards
+        mSocketRequests.remove(listener);
         mSocketProvider.unrequestSocket(callback);
     }
 
@@ -209,42 +209,28 @@
 
     private void sendMdnsPacket(@NonNull DatagramPacket packet, @NonNull SocketKey targetSocketKey,
             boolean onlyUseIpv6OnIpv6OnlyNetworks) {
+        final MdnsInterfaceSocket socket = getTargetSocket(targetSocketKey);
+        if (socket == null) {
+            mSharedLog.e("No socket matches targetSocketKey=" + targetSocketKey);
+            return;
+        }
+
         final boolean isIpv6 = ((InetSocketAddress) packet.getSocketAddress()).getAddress()
                 instanceof Inet6Address;
         final boolean isIpv4 = ((InetSocketAddress) packet.getSocketAddress()).getAddress()
                 instanceof Inet4Address;
-        final ArrayMap<MdnsInterfaceSocket, SocketKey> activeSockets = getActiveSockets();
-        boolean shouldQueryIpv6 = !onlyUseIpv6OnIpv6OnlyNetworks || isIpv6OnlySockets(
-                activeSockets, targetSocketKey);
-        for (int i = 0; i < activeSockets.size(); i++) {
-            final MdnsInterfaceSocket socket = activeSockets.keyAt(i);
-            final SocketKey socketKey = activeSockets.valueAt(i);
-            // Check ip capability and network before sending packet
-            if (((isIpv6 && socket.hasJoinedIpv6() && shouldQueryIpv6)
-                    || (isIpv4 && socket.hasJoinedIpv4()))
-                    && Objects.equals(socketKey, targetSocketKey)) {
-                try {
-                    socket.send(packet);
-                } catch (IOException e) {
-                    mSharedLog.e("Failed to send a mDNS packet.", e);
-                }
+        final boolean shouldQueryIpv6 = !onlyUseIpv6OnIpv6OnlyNetworks || !socket.hasJoinedIpv4();
+        // Check ip capability and network before sending packet
+        if ((isIpv6 && socket.hasJoinedIpv6() && shouldQueryIpv6)
+                || (isIpv4 && socket.hasJoinedIpv4())) {
+            try {
+                socket.send(packet);
+            } catch (IOException e) {
+                mSharedLog.e("Failed to send a mDNS packet.", e);
             }
         }
     }
 
-    private boolean isIpv6OnlySockets(
-            @NonNull ArrayMap<MdnsInterfaceSocket, SocketKey> activeSockets,
-            @NonNull SocketKey targetSocketKey) {
-        for (int i = 0; i < activeSockets.size(); i++) {
-            final MdnsInterfaceSocket socket = activeSockets.keyAt(i);
-            final SocketKey socketKey = activeSockets.valueAt(i);
-            if (Objects.equals(socketKey, targetSocketKey) && socket.hasJoinedIpv4()) {
-                return false;
-            }
-        }
-        return true;
-    }
-
     private void processResponsePacket(byte[] recvbuf, int length, @NonNull SocketKey socketKey) {
         int packetNumber = ++mReceivedPacketNumber;
 
diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
index ece10f3..0b54fdd 100644
--- a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
+++ b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
@@ -328,24 +328,24 @@
 
             @Override
             public void onProvisioningSuccess(LinkProperties newLp) {
-                safelyPostOnHandler(() -> onIpLayerStarted(newLp));
+                safelyPostOnHandler(() -> handleOnProvisioningSuccess(newLp));
             }
 
             @Override
             public void onProvisioningFailure(LinkProperties newLp) {
                 // This cannot happen due to provisioning timeout, because our timeout is 0. It can
                 // happen due to errors while provisioning or on provisioning loss.
-                safelyPostOnHandler(() -> onIpLayerStopped());
+                safelyPostOnHandler(() -> handleOnProvisioningFailure());
             }
 
             @Override
             public void onLinkPropertiesChange(LinkProperties newLp) {
-                safelyPostOnHandler(() -> updateLinkProperties(newLp));
+                safelyPostOnHandler(() -> handleOnLinkPropertiesChange(newLp));
             }
 
             @Override
             public void onReachabilityLost(String logMsg) {
-                safelyPostOnHandler(() -> updateNeighborLostEvent(logMsg));
+                safelyPostOnHandler(() -> handleOnReachabilityLost(logMsg));
             }
 
             @Override
@@ -499,7 +499,7 @@
             mIpClient.startProvisioning(createProvisioningConfiguration(mIpConfig));
         }
 
-        void onIpLayerStarted(@NonNull final LinkProperties linkProperties) {
+        private void handleOnProvisioningSuccess(@NonNull final LinkProperties linkProperties) {
             if (mNetworkAgent != null) {
                 Log.e(TAG, "Already have a NetworkAgent - aborting new request");
                 stop();
@@ -533,7 +533,7 @@
             mNetworkAgent.markConnected();
         }
 
-        void onIpLayerStopped() {
+        private void handleOnProvisioningFailure() {
             // There is no point in continuing if the interface is gone as stop() will be triggered
             // by removeInterface() when processed on the handler thread and start() won't
             // work for a non-existent interface.
@@ -553,15 +553,15 @@
             }
         }
 
-        void updateLinkProperties(LinkProperties linkProperties) {
+        private void handleOnLinkPropertiesChange(LinkProperties linkProperties) {
             mLinkProperties = linkProperties;
             if (mNetworkAgent != null) {
                 mNetworkAgent.sendLinkPropertiesImpl(linkProperties);
             }
         }
 
-        void updateNeighborLostEvent(String logMsg) {
-            Log.i(TAG, "updateNeighborLostEvent " + logMsg);
+        private void handleOnReachabilityLost(String logMsg) {
+            Log.i(TAG, "handleOnReachabilityLost " + logMsg);
             if (mIpConfig.getIpAssignment() == IpAssignment.STATIC) {
                 // Ignore NUD failures for static IP configurations, where restarting the IpClient
                 // will not fix connectivity.
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 8141350..48e86d8 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -325,7 +325,7 @@
     protected void unicastInterfaceStateChange(@NonNull IEthernetServiceListener listener,
             @NonNull String iface) {
         ensureRunningOnEthernetServiceThread();
-        final int state = mFactory.getInterfaceState(iface);
+        final int state = getInterfaceState(iface);
         final int role = getInterfaceRole(iface);
         final IpConfiguration config = getIpConfigurationForCallback(iface, state);
         try {
diff --git a/service/Android.bp b/service/Android.bp
index a65d664..9ae3d6c 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -237,6 +237,7 @@
         "service-connectivity-tiramisu-pre-jarjar",
         "service-nearby-pre-jarjar",
         "service-remoteauth-pre-jarjar",
+        "service-thread-pre-jarjar",
     ],
     // The below libraries are not actually needed to build since no source is compiled
     // (only combining prebuilt static_libs), but they are necessary so that R8 has the right
@@ -305,6 +306,7 @@
         ":service-connectivity-jarjar-gen",
         ":service-nearby-jarjar-gen",
         ":service-remoteauth-jarjar-gen",
+        ":service-thread-jarjar-gen",
     ],
     out: ["connectivity-jarjar-rules.txt"],
     visibility: ["//packages/modules/Connectivity:__subpackages__"],
@@ -374,6 +376,24 @@
     visibility: ["//visibility:private"],
 }
 
+java_genrule {
+    name: "service-thread-jarjar-gen",
+    tool_files: [
+        ":service-thread-pre-jarjar{.jar}",
+        "jarjar-excludes.txt",
+    ],
+    tools: [
+        "jarjar-rules-generator",
+    ],
+    out: ["service_thread_jarjar_rules.txt"],
+    cmd: "$(location jarjar-rules-generator) " +
+        "$(location :service-thread-pre-jarjar{.jar}) " +
+        "--prefix com.android.server.thread " +
+        "--excludes $(location jarjar-excludes.txt) " +
+        "--output $(out)",
+    visibility: ["//visibility:private"],
+}
+
 genrule {
     name: "statslog-connectivity-java-gen",
     tools: ["stats-log-api-gen"],
diff --git a/service/native/TrafficController.cpp b/service/native/TrafficController.cpp
index 8f6df21..3828389 100644
--- a/service/native/TrafficController.cpp
+++ b/service/native/TrafficController.cpp
@@ -576,53 +576,12 @@
     }
 }
 
-std::string getMapStatus(const base::unique_fd& map_fd, const char* path) {
-    if (map_fd.get() < 0) {
-        return StringPrintf("map fd lost");
-    }
-    if (access(path, F_OK) != 0) {
-        return StringPrintf("map not pinned to location: %s", path);
-    }
-    return StringPrintf("OK");
-}
-
-// NOLINTNEXTLINE(google-runtime-references): grandfathered pass by non-const reference
-void dumpBpfMap(const std::string& mapName, DumpWriter& dw, const std::string& header) {
-    dw.blankline();
-    dw.println("%s:", mapName.c_str());
-    if (!header.empty()) {
-        dw.println(header);
-    }
-}
-
 void TrafficController::dump(int fd, bool verbose __unused) {
     std::lock_guard guard(mMutex);
     DumpWriter dw(fd);
 
     ScopedIndent indentTop(dw);
     dw.println("TrafficController");
-
-    ScopedIndent indentPreBpfModule(dw);
-
-    dw.blankline();
-    dw.println("mCookieTagMap status: %s",
-               getMapStatus(mCookieTagMap.getMap(), COOKIE_TAG_MAP_PATH).c_str());
-    dw.println("mUidCounterSetMap status: %s",
-               getMapStatus(mUidCounterSetMap.getMap(), UID_COUNTERSET_MAP_PATH).c_str());
-    dw.println("mAppUidStatsMap status: %s",
-               getMapStatus(mAppUidStatsMap.getMap(), APP_UID_STATS_MAP_PATH).c_str());
-    dw.println("mStatsMapA status: %s",
-               getMapStatus(mStatsMapA.getMap(), STATS_MAP_A_PATH).c_str());
-    dw.println("mStatsMapB status: %s",
-               getMapStatus(mStatsMapB.getMap(), STATS_MAP_B_PATH).c_str());
-    dw.println("mIfaceIndexNameMap status: %s",
-               getMapStatus(mIfaceIndexNameMap.getMap(), IFACE_INDEX_NAME_MAP_PATH).c_str());
-    dw.println("mIfaceStatsMap status: %s",
-               getMapStatus(mIfaceStatsMap.getMap(), IFACE_STATS_MAP_PATH).c_str());
-    dw.println("mConfigurationMap status: %s",
-               getMapStatus(mConfigurationMap.getMap(), CONFIGURATION_MAP_PATH).c_str());
-    dw.println("mUidOwnerMap status: %s",
-               getMapStatus(mUidOwnerMap.getMap(), UID_OWNER_MAP_PATH).c_str());
 }
 
 }  // namespace net
diff --git a/service/native/TrafficControllerTest.cpp b/service/native/TrafficControllerTest.cpp
index 57f32af..99e9831 100644
--- a/service/native/TrafficControllerTest.cpp
+++ b/service/native/TrafficControllerTest.cpp
@@ -269,109 +269,6 @@
         }
         return ret;
     }
-
-    Status dump(bool verbose, std::vector<std::string>& outputLines) {
-      if (!outputLines.empty()) return statusFromErrno(EUCLEAN, "Output buffer is not empty");
-
-      android::base::unique_fd localFd, remoteFd;
-      if (!Pipe(&localFd, &remoteFd)) return statusFromErrno(errno, "Failed on pipe");
-
-      // dump() blocks until another thread has consumed all its output.
-      std::thread dumpThread =
-          std::thread([this, remoteFd{std::move(remoteFd)}, verbose]() {
-            mTc.dump(remoteFd, verbose);
-          });
-
-      std::string dumpContent;
-      if (!android::base::ReadFdToString(localFd.get(), &dumpContent)) {
-        return statusFromErrno(errno, "Failed to read dump results from fd");
-      }
-      dumpThread.join();
-
-      std::stringstream dumpStream(std::move(dumpContent));
-      std::string line;
-      while (std::getline(dumpStream, line)) {
-        outputLines.push_back(line);
-      }
-
-      return netdutils::status::ok;
-    }
-
-    // Strings in the |expect| must exist in dump results in order. But no need to be consecutive.
-    bool expectDumpsysContains(std::vector<std::string>& expect) {
-        if (expect.empty()) return false;
-
-        std::vector<std::string> output;
-        Status result = dump(true, output);
-        if (!isOk(result)) {
-            GTEST_LOG_(ERROR) << "TrafficController dump failed: " << netdutils::toString(result);
-            return false;
-        }
-
-        int matched = 0;
-        auto it = expect.begin();
-        for (const auto& line : output) {
-            if (it == expect.end()) break;
-            if (std::string::npos != line.find(*it)) {
-                matched++;
-                ++it;
-            }
-        }
-
-        if (matched != expect.size()) {
-            // dump results for debugging
-            for (const auto& o : output) LOG(INFO) << "output: " << o;
-            for (const auto& e : expect) LOG(INFO) << "expect: " << e;
-            return false;
-        }
-        return true;
-    }
-
-    // Once called, the maps of TrafficController can't recover to valid maps which initialized
-    // in SetUp().
-    void makeTrafficControllerMapsInvalid() {
-        constexpr char INVALID_PATH[] = "invalid";
-
-        mFakeCookieTagMap.init(INVALID_PATH);
-        mTc.mCookieTagMap = mFakeCookieTagMap;
-        ASSERT_INVALID(mTc.mCookieTagMap);
-
-        mFakeAppUidStatsMap.init(INVALID_PATH);
-        mTc.mAppUidStatsMap = mFakeAppUidStatsMap;
-        ASSERT_INVALID(mTc.mAppUidStatsMap);
-
-        mFakeStatsMapA.init(INVALID_PATH);
-        mTc.mStatsMapA = mFakeStatsMapA;
-        ASSERT_INVALID(mTc.mStatsMapA);
-
-        mFakeStatsMapB.init(INVALID_PATH);
-        mTc.mStatsMapB = mFakeStatsMapB;
-        ASSERT_INVALID(mTc.mStatsMapB);
-
-        mFakeIfaceStatsMap.init(INVALID_PATH);
-        mTc.mIfaceStatsMap = mFakeIfaceStatsMap;
-        ASSERT_INVALID(mTc.mIfaceStatsMap);
-
-        mFakeConfigurationMap.init(INVALID_PATH);
-        mTc.mConfigurationMap = mFakeConfigurationMap;
-        ASSERT_INVALID(mTc.mConfigurationMap);
-
-        mFakeUidOwnerMap.init(INVALID_PATH);
-        mTc.mUidOwnerMap = mFakeUidOwnerMap;
-        ASSERT_INVALID(mTc.mUidOwnerMap);
-
-        mFakeUidPermissionMap.init(INVALID_PATH);
-        mTc.mUidPermissionMap = mFakeUidPermissionMap;
-        ASSERT_INVALID(mTc.mUidPermissionMap);
-
-        mFakeUidCounterSetMap.init(INVALID_PATH);
-        mTc.mUidCounterSetMap = mFakeUidCounterSetMap;
-        ASSERT_INVALID(mTc.mUidCounterSetMap);
-
-        mFakeIfaceIndexNameMap.init(INVALID_PATH);
-        mTc.mIfaceIndexNameMap = mFakeIfaceIndexNameMap;
-        ASSERT_INVALID(mTc.mIfaceIndexNameMap);
-    }
 };
 
 TEST_F(TrafficControllerTest, TestUpdateOwnerMapEntry) {
diff --git a/service/native/include/TrafficController.h b/service/native/include/TrafficController.h
index cb6c836..d610d25 100644
--- a/service/native/include/TrafficController.h
+++ b/service/native/include/TrafficController.h
@@ -33,8 +33,6 @@
 
 class TrafficController {
   public:
-    static constexpr char DUMP_KEYWORD[] = "trafficcontroller";
-
     /*
      * Initialize the whole controller
      */
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index 7aff6a4..2842cc3 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -43,7 +43,6 @@
 import android.os.Build;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
-import android.provider.DeviceConfig;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.util.ArraySet;
@@ -95,8 +94,8 @@
     private static boolean sInitialized = false;
 
     private static Boolean sEnableJavaBpfMap = null;
-    private static final String BPF_NET_MAPS_ENABLE_JAVA_BPF_MAP =
-            "bpf_net_maps_enable_java_bpf_map";
+    private static final String BPF_NET_MAPS_FORCE_DISABLE_JAVA_BPF_MAP =
+            "bpf_net_maps_force_disable_java_bpf_map";
 
     // Lock for sConfigurationMap entry for UID_RULES_CONFIGURATION_KEY.
     // This entry is not accessed by others.
@@ -283,9 +282,8 @@
         if (sInitialized) return;
         if (sEnableJavaBpfMap == null) {
             sEnableJavaBpfMap = SdkLevel.isAtLeastU() ||
-                    DeviceConfigUtils.isFeatureEnabled(context,
-                            DeviceConfig.NAMESPACE_TETHERING, BPF_NET_MAPS_ENABLE_JAVA_BPF_MAP,
-                            DeviceConfigUtils.TETHERING_MODULE_NAME, false /* defaultValue */);
+                    DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
+                            BPF_NET_MAPS_FORCE_DISABLE_JAVA_BPF_MAP);
         }
         Log.d(TAG, "BpfNetMaps is initialized with sEnableJavaBpfMap=" + sEnableJavaBpfMap);
 
@@ -1143,7 +1141,7 @@
         }
     }
 
-    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private static native void native_init(boolean startSkDestroyListener);
 
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index a29f47f..04d0b93 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -318,7 +318,6 @@
 import java.io.InterruptedIOException;
 import java.io.PrintWriter;
 import java.io.Writer;
-import java.lang.IllegalArgumentException;
 import java.net.Inet4Address;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
@@ -1413,10 +1412,10 @@
         }
 
         /**
-         * @see DeviceConfigUtils#isFeatureEnabled
+         * @see DeviceConfigUtils#isTetheringFeatureEnabled
          */
         public boolean isFeatureEnabled(Context context, String name) {
-            return DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_TETHERING, name,
+            return DeviceConfigUtils.isTetheringFeatureEnabled(context, NAMESPACE_TETHERING, name,
                     TETHERING_MODULE_NAME, false /* defaultValue */);
         }
 
@@ -11108,16 +11107,20 @@
 
         @Override
         public void onInterfaceLinkStateChanged(@NonNull String iface, boolean up) {
-            for (NetworkAgentInfo nai : mNetworkAgentInfos) {
-                nai.clatd.interfaceLinkStateChanged(iface, up);
-            }
+            mHandler.post(() -> {
+                for (NetworkAgentInfo nai : mNetworkAgentInfos) {
+                    nai.clatd.interfaceLinkStateChanged(iface, up);
+                }
+            });
         }
 
         @Override
         public void onInterfaceRemoved(@NonNull String iface) {
-            for (NetworkAgentInfo nai : mNetworkAgentInfos) {
-                nai.clatd.interfaceRemoved(iface);
-            }
+            mHandler.post(() -> {
+                for (NetworkAgentInfo nai : mNetworkAgentInfos) {
+                    nai.clatd.interfaceRemoved(iface);
+                }
+            });
         }
     }
 
diff --git a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
index 4f2909c..3befcfa 100644
--- a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
+++ b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
@@ -20,7 +20,6 @@
 import static android.net.SocketKeepalive.MIN_INTERVAL_SEC;
 import static android.net.SocketKeepalive.SUCCESS;
 import static android.net.SocketKeepalive.SUCCESS_PAUSED;
-import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
 import static android.system.OsConstants.AF_INET;
 import static android.system.OsConstants.AF_INET6;
 import static android.system.OsConstants.SOL_SOCKET;
@@ -92,8 +91,8 @@
     private static final int[] ADDRESS_FAMILIES = new int[] {AF_INET6, AF_INET};
     private static final long LOW_TCP_POLLING_INTERVAL_MS = 1_000L;
     private static final int ADJUST_TCP_POLLING_DELAY_MS = 2000;
-    private static final String AUTOMATIC_ON_OFF_KEEPALIVE_VERSION =
-            "automatic_on_off_keepalive_version";
+    private static final String AUTOMATIC_ON_OFF_KEEPALIVE_DISABLE_FLAG =
+            "automatic_on_off_keepalive_disable_flag";
     public static final long METRICS_COLLECTION_DURATION_MS = 24 * 60 * 60 * 1_000L;
 
     // ConnectivityService parses message constants from itself and AutomaticOnOffKeepaliveTracker
@@ -219,8 +218,8 @@
             // Reading DeviceConfig will check if the calling uid and calling package name are the
             // same. Clear calling identity to align the calling uid and package
             final boolean enabled = BinderUtils.withCleanCallingIdentity(
-                    () -> mDependencies.isFeatureEnabled(AUTOMATIC_ON_OFF_KEEPALIVE_VERSION,
-                            true /* defaultEnabled */));
+                    () -> mDependencies.isTetheringFeatureNotChickenedOut(
+                            AUTOMATIC_ON_OFF_KEEPALIVE_DISABLE_FLAG));
             if (autoOnOff && enabled) {
                 mAutomaticOnOffState = STATE_ENABLED;
                 if (null == ki.mFd) {
@@ -700,8 +699,8 @@
         // Clear calling identity to align the calling uid and package so that it won't fail if cts
         // would like to call dump()
         final boolean featureEnabled = BinderUtils.withCleanCallingIdentity(
-                () -> mDependencies.isFeatureEnabled(AUTOMATIC_ON_OFF_KEEPALIVE_VERSION,
-                        true /* defaultEnabled */));
+                () -> mDependencies.isTetheringFeatureNotChickenedOut(
+                        AUTOMATIC_ON_OFF_KEEPALIVE_DISABLE_FLAG));
         pw.println("AutomaticOnOff enabled: " + featureEnabled);
         pw.increaseIndent();
         for (AutomaticOnOffKeepalive autoKi : mAutomaticOnOffKeepalives) {
@@ -969,16 +968,13 @@
         }
 
         /**
-         * Find out if a feature is enabled from DeviceConfig.
+         * Find out if a feature is not disabled from DeviceConfig.
          *
          * @param name The name of the property to look up.
-         * @param defaultEnabled whether to consider the feature enabled in the absence of
-         *                       the flag. This MUST be a statically-known constant.
          * @return whether the feature is enabled
          */
-        public boolean isFeatureEnabled(@NonNull final String name, final boolean defaultEnabled) {
-            return DeviceConfigUtils.isFeatureEnabled(mContext, NAMESPACE_TETHERING, name,
-                    DeviceConfigUtils.TETHERING_MODULE_NAME, defaultEnabled);
+        public boolean isTetheringFeatureNotChickenedOut(@NonNull final String name) {
+            return DeviceConfigUtils.isTetheringFeatureNotChickenedOut(name);
         }
 
         /**
diff --git a/service/src/com/android/server/connectivity/KeepaliveTracker.java b/service/src/com/android/server/connectivity/KeepaliveTracker.java
index ae70767..feba821 100644
--- a/service/src/com/android/server/connectivity/KeepaliveTracker.java
+++ b/service/src/com/android/server/connectivity/KeepaliveTracker.java
@@ -993,7 +993,7 @@
          */
         public boolean isAddressTranslationEnabled(@NonNull Context context) {
             return DeviceConfigUtils.isFeatureSupported(context, FEATURE_CLAT_ADDRESS_TRANSLATE)
-                    && !DeviceConfigUtils.isTetheringFeatureForceDisabled(
+                    && DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
                             CONFIG_DISABLE_CLAT_ADDRESS_TRANSLATE);
         }
     }
diff --git a/tests/common/java/android/net/KeepalivePacketDataTest.kt b/tests/common/java/android/net/KeepalivePacketDataTest.kt
index f464ec6..403d6b5 100644
--- a/tests/common/java/android/net/KeepalivePacketDataTest.kt
+++ b/tests/common/java/android/net/KeepalivePacketDataTest.kt
@@ -22,6 +22,7 @@
 import androidx.test.runner.AndroidJUnit4
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.NonNullTestUtils
 import java.net.InetAddress
 import java.util.Arrays
 import org.junit.Assert.assertEquals
@@ -55,43 +56,42 @@
         dstAddress: InetAddress? = TEST_DST_ADDRV4,
         dstPort: Int = TEST_DST_PORT,
         data: ByteArray = TESTBYTES
-    ) : KeepalivePacketData(srcAddress, srcPort, dstAddress, dstPort, data)
+    ) : KeepalivePacketData(NonNullTestUtils.nullUnsafe(srcAddress), srcPort,
+            NonNullTestUtils.nullUnsafe(dstAddress), dstPort, data)
 
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.Q)
     fun testConstructor() {
-        var data: TestKeepalivePacketData
-
         try {
-            data = TestKeepalivePacketData(srcAddress = null)
+            TestKeepalivePacketData(srcAddress = null)
             fail("Null src address should cause exception")
         } catch (e: InvalidPacketException) {
             assertEquals(e.error, ERROR_INVALID_IP_ADDRESS)
         }
 
         try {
-            data = TestKeepalivePacketData(dstAddress = null)
+            TestKeepalivePacketData(dstAddress = null)
             fail("Null dst address should cause exception")
         } catch (e: InvalidPacketException) {
             assertEquals(e.error, ERROR_INVALID_IP_ADDRESS)
         }
 
         try {
-            data = TestKeepalivePacketData(dstAddress = TEST_ADDRV6)
+            TestKeepalivePacketData(dstAddress = TEST_ADDRV6)
             fail("Ip family mismatched should cause exception")
         } catch (e: InvalidPacketException) {
             assertEquals(e.error, ERROR_INVALID_IP_ADDRESS)
         }
 
         try {
-            data = TestKeepalivePacketData(srcPort = INVALID_PORT)
+            TestKeepalivePacketData(srcPort = INVALID_PORT)
             fail("Invalid srcPort should cause exception")
         } catch (e: InvalidPacketException) {
             assertEquals(e.error, ERROR_INVALID_PORT)
         }
 
         try {
-            data = TestKeepalivePacketData(dstPort = INVALID_PORT)
+            TestKeepalivePacketData(dstPort = INVALID_PORT)
             fail("Invalid dstPort should cause exception")
         } catch (e: InvalidPacketException) {
             assertEquals(e.error, ERROR_INVALID_PORT)
@@ -117,4 +117,4 @@
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.Q)
     fun testPacket() = assertTrue(Arrays.equals(TESTBYTES, TestKeepalivePacketData().packet))
-}
\ No newline at end of file
+}
diff --git a/tests/common/java/android/net/LinkPropertiesTest.java b/tests/common/java/android/net/LinkPropertiesTest.java
index 09f5d6e..d2e7c99 100644
--- a/tests/common/java/android/net/LinkPropertiesTest.java
+++ b/tests/common/java/android/net/LinkPropertiesTest.java
@@ -1260,7 +1260,7 @@
         assertFalse(lp.hasIpv4UnreachableDefaultRoute());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
     @CtsNetTestCasesMaxTargetSdk31(reason = "Compat change cannot be overridden when targeting T+")
     @EnableCompatChanges({ConnectivityCompatChanges.EXCLUDED_ROUTES})
     public void testHasExcludeRoute() {
@@ -1273,7 +1273,7 @@
         assertTrue(lp.hasExcludeRoute());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
     @CtsNetTestCasesMaxTargetSdk31(reason = "Compat change cannot be overridden when targeting T+")
     @EnableCompatChanges({ConnectivityCompatChanges.EXCLUDED_ROUTES})
     public void testRouteAddWithSameKey() throws Exception {
@@ -1347,14 +1347,14 @@
         assertExcludeRoutesVisible();
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
     @CtsNetTestCasesMaxTargetSdk31(reason = "Compat change cannot be overridden when targeting T+")
     @EnableCompatChanges({ConnectivityCompatChanges.EXCLUDED_ROUTES})
     public void testExcludedRoutesEnabledByCompatChange() {
         assertExcludeRoutesVisible();
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
     @CtsNetTestCasesMaxTargetSdk31(reason = "Compat change cannot be overridden when targeting T+")
     @DisableCompatChanges({ConnectivityCompatChanges.EXCLUDED_ROUTES})
     public void testExcludedRoutesDisabledByCompatChange() {
diff --git a/tests/common/java/android/net/NetworkProviderTest.kt b/tests/common/java/android/net/NetworkProviderTest.kt
index fcbb0dd..c6a7346 100644
--- a/tests/common/java/android/net/NetworkProviderTest.kt
+++ b/tests/common/java/android/net/NetworkProviderTest.kt
@@ -67,7 +67,7 @@
 class NetworkProviderTest {
     @Rule @JvmField
     val mIgnoreRule = DevSdkIgnoreRule()
-    private val mCm = context.getSystemService(ConnectivityManager::class.java)
+    private val mCm = context.getSystemService(ConnectivityManager::class.java)!!
     private val mHandlerThread = HandlerThread("${javaClass.simpleName} handler thread")
 
     @Before
diff --git a/tests/cts/OWNERS b/tests/cts/OWNERS
index 286f9c8..cb4ca59 100644
--- a/tests/cts/OWNERS
+++ b/tests/cts/OWNERS
@@ -5,3 +5,8 @@
 
 # IPsec
 per-file **IpSec* = benedictwong@google.com, nharold@google.com
+
+# For incremental changes on EthernetManagerTest to increase coverage for existing behavior and for
+# testing bug fixes.
+per-file net/src/android/net/cts/EthernetManagerTest.kt = prohr@google.com #{LAST_RESORT_SUGGESTION}
+
diff --git a/tests/cts/net/native/dns/Android.bp b/tests/cts/net/native/dns/Android.bp
index 2469710..da4fe28 100644
--- a/tests/cts/net/native/dns/Android.bp
+++ b/tests/cts/net/native/dns/Android.bp
@@ -30,6 +30,7 @@
     ],
     // To be compatible with R devices, the min_sdk_version must be 30.
     min_sdk_version: "30",
+    host_required: ["net-tests-utils-host-common"],
 }
 
 cc_test {
diff --git a/tests/cts/net/native/dns/AndroidTest.xml b/tests/cts/net/native/dns/AndroidTest.xml
index 6d03c23..d49696b 100644
--- a/tests/cts/net/native/dns/AndroidTest.xml
+++ b/tests/cts/net/native/dns/AndroidTest.xml
@@ -24,6 +24,8 @@
         <option name="push" value="CtsNativeNetDnsTestCases->/data/local/tmp/CtsNativeNetDnsTestCases" />
         <option name="append-bitness" value="true" />
     </target_preparer>
+    <target_preparer class="com.android.testutils.ConnectivityTestTargetPreparer">
+    </target_preparer>
     <test class="com.android.tradefed.testtype.GTest" >
         <option name="native-test-device-path" value="/data/local/tmp" />
         <option name="module-name" value="CtsNativeNetDnsTestCases" />
diff --git a/tests/cts/net/src/android/net/cts/BatteryStatsManagerTest.java b/tests/cts/net/src/android/net/cts/BatteryStatsManagerTest.java
index 3c71c90..466514c 100644
--- a/tests/cts/net/src/android/net/cts/BatteryStatsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/BatteryStatsManagerTest.java
@@ -18,6 +18,7 @@
 
 import static android.Manifest.permission.UPDATE_DEVICE_STATS;
 import static android.content.pm.PackageManager.FEATURE_TELEPHONY;
+import static android.content.pm.PackageManager.FEATURE_WIFI;
 
 import static androidx.test.InstrumentationRegistry.getContext;
 
@@ -118,8 +119,10 @@
             // side effect is the point of using --write here.
             executeShellCommand("dumpsys batterystats --write");
 
-            // Make sure wifi is disabled.
-            mCtsNetUtils.ensureWifiDisconnected(null /* wifiNetworkToCheck */);
+            if (mPm.hasSystemFeature(FEATURE_WIFI)) {
+                // Make sure wifi is disabled.
+                mCtsNetUtils.ensureWifiDisconnected(null /* wifiNetworkToCheck */);
+            }
 
             verifyGetCellBatteryStats();
             verifyGetWifiBatteryStats();
@@ -128,6 +131,9 @@
             // Reset battery settings.
             executeShellCommand("dumpsys batterystats disable no-auto-reset");
             executeShellCommand("cmd battery reset");
+            if (mPm.hasSystemFeature(FEATURE_WIFI)) {
+                mCtsNetUtils.ensureWifiConnected();
+            }
         }
     }
 
@@ -153,23 +159,31 @@
         // The mobile battery stats are updated when a network stops being the default network.
         // ConnectivityService will call BatteryStatsManager.reportMobileRadioPowerState when
         // removing data activity tracking.
-        mCtsNetUtils.ensureWifiConnected();
+        try {
+            mCtsNetUtils.setMobileDataEnabled(false);
 
-        // There's rate limit to update mobile battery so if ConnectivityService calls
-        // BatteryStatsManager.reportMobileRadioPowerState when default network changed,
-        // the mobile stats might not be updated. But if the mobile update due to other
-        // reasons (plug/unplug, battery level change, etc) will be unaffected. Thus here
-        // dumps the battery stats to trigger a full sync of data.
-        executeShellCommand("dumpsys batterystats");
+            // There's rate limit to update mobile battery so if ConnectivityService calls
+            // BatteryStatsManager.reportMobileRadioPowerState when default network changed,
+            // the mobile stats might not be updated. But if the mobile update due to other
+            // reasons (plug/unplug, battery level change, etc) will be unaffected. Thus here
+            // dumps the battery stats to trigger a full sync of data.
+            executeShellCommand("dumpsys batterystats");
 
-        // Check cellular battery stats are updated.
-        runAsShell(UPDATE_DEVICE_STATS,
-                () -> assertStatsEventually(mBsm::getCellularBatteryStats,
-                    cellularStatsAfter -> cellularBatteryStatsIncreased(
-                    cellularStatsBefore, cellularStatsAfter)));
+            // Check cellular battery stats are updated.
+            runAsShell(UPDATE_DEVICE_STATS,
+                    () -> assertStatsEventually(mBsm::getCellularBatteryStats,
+                        cellularStatsAfter -> cellularBatteryStatsIncreased(
+                        cellularStatsBefore, cellularStatsAfter)));
+        } finally {
+            mCtsNetUtils.setMobileDataEnabled(true);
+        }
     }
 
     private void verifyGetWifiBatteryStats() throws Exception {
+        if (!mPm.hasSystemFeature(FEATURE_WIFI)) {
+            return;
+        }
+
         final Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
         final URL url = new URL(TEST_URL);
 
@@ -199,9 +213,9 @@
     @Test
     public void testReportNetworkInterfaceForTransports_throwsSecurityException()
             throws Exception {
-        Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
-        final String iface = mCm.getLinkProperties(wifiNetwork).getInterfaceName();
-        final int[] transportType = mCm.getNetworkCapabilities(wifiNetwork).getTransportTypes();
+        final Network network = mCm.getActiveNetwork();
+        final String iface = mCm.getLinkProperties(network).getInterfaceName();
+        final int[] transportType = mCm.getNetworkCapabilities(network).getTransportTypes();
         assertThrows(SecurityException.class,
                 () -> mBsm.reportNetworkInterfaceForTransports(iface, transportType));
     }
diff --git a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
index 1d79806..99222dd 100644
--- a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
+++ b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
@@ -95,7 +95,7 @@
 @RunWith(AndroidJUnit4::class)
 class CaptivePortalTest {
     private val context: android.content.Context by lazy { getInstrumentation().context }
-    private val cm by lazy { context.getSystemService(ConnectivityManager::class.java) }
+    private val cm by lazy { context.getSystemService(ConnectivityManager::class.java)!! }
     private val pm by lazy { context.packageManager }
     private val utils by lazy { CtsNetUtils(context) }
 
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index e978664..77cea1a 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -1025,11 +1025,13 @@
         final String goodPrivateDnsServer = "dns.google";
         mCtsNetUtils.storePrivateDnsSetting();
         final TestableNetworkCallback cb = new TestableNetworkCallback();
-        registerNetworkCallback(makeWifiNetworkRequest(), cb);
+        final NetworkRequest networkRequest = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_INTERNET).build();
+        registerNetworkCallback(networkRequest, cb);
+        final Network networkForPrivateDns = mCm.getActiveNetwork();
         try {
             // Verifying the good private DNS sever
             mCtsNetUtils.setPrivateDnsStrictMode(goodPrivateDnsServer);
-            final Network networkForPrivateDns =  mCtsNetUtils.ensureWifiConnected();
             cb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED, NETWORK_CALLBACK_TIMEOUT_MS,
                     entry -> hasPrivateDnsValidated(entry, networkForPrivateDns));
 
@@ -1040,8 +1042,11 @@
                     .isPrivateDnsBroken()) && networkForPrivateDns.equals(entry.getNetwork()));
         } finally {
             mCtsNetUtils.restorePrivateDnsSetting();
-            // Toggle wifi to make sure it is re-validated
-            reconnectWifi();
+            // Toggle network to make sure it is re-validated
+            mCm.reportNetworkConnectivity(networkForPrivateDns, true);
+            cb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED, NETWORK_CALLBACK_TIMEOUT_MS,
+                    entry -> !(((CallbackEntry.CapabilitiesChanged) entry).getCaps()
+                    .isPrivateDnsBroken()) && networkForPrivateDns.equals(entry.getNetwork()));
         }
     }
 
@@ -1306,9 +1311,12 @@
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
     @Test
     public void testRequestNetworkCallback_onUnavailable() {
-        final boolean previousWifiEnabledState = mWifiManager.isWifiEnabled();
-        if (previousWifiEnabledState) {
-            mCtsNetUtils.ensureWifiDisconnected(null);
+        boolean previousWifiEnabledState = false;
+        if (mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
+            previousWifiEnabledState = mWifiManager.isWifiEnabled();
+            if (previousWifiEnabledState) {
+                mCtsNetUtils.ensureWifiDisconnected(null);
+            }
         }
 
         final TestNetworkCallback callback = new TestNetworkCallback();
@@ -1344,6 +1352,8 @@
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
     @Test
     public void testToggleWifiConnectivityAction() throws Exception {
+        assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
+
         // toggleWifi calls connectToWifi and disconnectFromWifi, which both wait for
         // CONNECTIVITY_ACTION broadcasts.
         mCtsNetUtils.toggleWifi();
@@ -2744,7 +2754,6 @@
      */
     @AppModeFull(reason = "Instant apps cannot create test networks")
     @Test
-    @SkipMainlinePresubmit(reason = "Out of SLO flakiness")
     public void testSetOemNetworkPreferenceForTestOnlyPref() throws Exception {
         // Cannot use @IgnoreUpTo(Build.VERSION_CODES.R) because this test also requires API 31
         // shims, and @IgnoreUpTo does not check that.
@@ -2757,17 +2766,19 @@
         final TestableNetworkCallback systemDefaultCallback = new TestableNetworkCallback();
 
         final Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
+        final Network testNetwork = tnt.getNetwork();
 
         testAndCleanup(() -> {
             setOemNetworkPreferenceForMyPackage(
                     OemNetworkPreferences.OEM_NETWORK_PREFERENCE_TEST_ONLY);
             registerTestOemNetworkPreferenceCallbacks(defaultCallback, systemDefaultCallback);
-            waitForAvailable(defaultCallback, tnt.getNetwork());
+            waitForAvailable(defaultCallback, testNetwork);
             systemDefaultCallback.eventuallyExpect(CallbackEntry.AVAILABLE,
                     NETWORK_CALLBACK_TIMEOUT_MS, cb -> wifiNetwork.equals(cb.getNetwork()));
         }, /* cleanup */ () -> {
                 runWithShellPermissionIdentity(tnt::teardown);
-                defaultCallback.expect(CallbackEntry.LOST, tnt, NETWORK_CALLBACK_TIMEOUT_MS);
+                defaultCallback.eventuallyExpect(CallbackEntry.LOST, NETWORK_CALLBACK_TIMEOUT_MS,
+                        cb -> testNetwork.equals(cb.getNetwork()));
 
                 // This network preference should only ever use the test network therefore available
                 // should not trigger when the test network goes down (e.g. switch to cellular).
@@ -3205,7 +3216,6 @@
 
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
     @Test
-    @SkipPresubmit(reason = "Out of SLO flakiness")
     public void testMobileDataPreferredUids() throws Exception {
         assumeTrue(TestUtils.shouldTestSApis());
         final boolean canRunTest = mPackageManager.hasSystemFeature(FEATURE_WIFI)
diff --git a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
index db13c49..f73134a 100644
--- a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
+++ b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
@@ -125,7 +125,7 @@
     private val TEST_TARGET_MAC_ADDR = MacAddress.fromString("12:34:56:78:9a:bc")
 
     private val realContext = InstrumentationRegistry.getContext()
-    private val cm = realContext.getSystemService(ConnectivityManager::class.java)
+    private val cm = realContext.getSystemService(ConnectivityManager::class.java)!!
 
     private val agentsToCleanUp = mutableListOf<NetworkAgent>()
     private val callbacksToCleanUp = mutableListOf<TestableNetworkCallback>()
@@ -160,7 +160,7 @@
         assumeTrue(kernelIsAtLeast(5, 15))
 
         runAsShell(MANAGE_TEST_NETWORKS) {
-            val tnm = realContext.getSystemService(TestNetworkManager::class.java)
+            val tnm = realContext.getSystemService(TestNetworkManager::class.java)!!
 
             // Only statically configure the IPv4 address; for IPv6, use the SLAAC generated
             // address.
@@ -306,7 +306,7 @@
 
         val socket = Os.socket(if (sendV6) AF_INET6 else AF_INET, SOCK_DGRAM or SOCK_NONBLOCK,
                 IPPROTO_UDP)
-        agent.network.bindSocket(socket)
+        checkNotNull(agent.network).bindSocket(socket)
 
         val originalPacket = testPacket.readAsArray()
         Os.sendto(socket, originalPacket, 0 /* bytesOffset */, originalPacket.size, 0 /* flags */,
diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
index 732a42b..3146b41 100644
--- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
@@ -92,7 +92,6 @@
 import kotlin.test.assertNotNull
 import kotlin.test.assertNull
 import kotlin.test.assertTrue
-import kotlin.test.fail
 import org.junit.After
 import org.junit.Assume.assumeFalse
 import org.junit.Assume.assumeTrue
@@ -135,8 +134,8 @@
 class EthernetManagerTest {
 
     private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
-    private val em by lazy { context.getSystemService(EthernetManager::class.java) }
-    private val cm by lazy { context.getSystemService(ConnectivityManager::class.java) }
+    private val em by lazy { context.getSystemService(EthernetManager::class.java)!! }
+    private val cm by lazy { context.getSystemService(ConnectivityManager::class.java)!! }
     private val handler by lazy { Handler(Looper.getMainLooper()) }
 
     private val ifaceListener = EthernetStateListener()
@@ -160,7 +159,7 @@
 
         init {
             tnm = runAsShell(MANAGE_TEST_NETWORKS) {
-                context.getSystemService(TestNetworkManager::class.java)
+                context.getSystemService(TestNetworkManager::class.java)!!
             }
             tapInterface = runAsShell(MANAGE_TEST_NETWORKS) {
                 // Configuring a tun/tap interface always enables the carrier. If hasCarrier is
@@ -254,7 +253,7 @@
         }
 
         fun <T : CallbackEntry> expectCallback(expected: T): T {
-            val event = pollOrThrow()
+            val event = events.poll(TIMEOUT_MS)
             assertEquals(expected, event)
             return event as T
         }
@@ -267,24 +266,21 @@
             expectCallback(EthernetStateChanged(state))
         }
 
-        fun createChangeEvent(iface: String, state: Int, role: Int) =
+        private fun createChangeEvent(iface: String, state: Int, role: Int) =
                 InterfaceStateChanged(iface, state, role,
                         if (state != STATE_ABSENT) DEFAULT_IP_CONFIGURATION else null)
 
-        fun pollOrThrow(): CallbackEntry {
-            return events.poll(TIMEOUT_MS) ?: fail("Did not receive callback after ${TIMEOUT_MS}ms")
+        fun eventuallyExpect(expected: CallbackEntry) {
+            val cb = events.poll(TIMEOUT_MS) { it == expected }
+            assertNotNull(cb, "Never received expected $expected. Received: ${events.backtrace()}")
         }
 
-        fun eventuallyExpect(expected: CallbackEntry) = events.poll(TIMEOUT_MS) { it == expected }
-
         fun eventuallyExpect(iface: EthernetTestInterface, state: Int, role: Int) {
-            val event = createChangeEvent(iface.name, state, role)
-            assertNotNull(eventuallyExpect(event), "Never received expected $event")
+            eventuallyExpect(createChangeEvent(iface.name, state, role))
         }
 
         fun eventuallyExpect(state: Int) {
-            val event = EthernetStateChanged(state)
-            assertNotNull(eventuallyExpect(event), "Never received expected $event")
+            eventuallyExpect(EthernetStateChanged(state))
         }
 
         fun assertNoCallback() {
@@ -652,9 +648,8 @@
 
         val listener = EthernetStateListener()
         addInterfaceStateListener(listener)
-        // Note: using eventuallyExpect as there may be other interfaces present.
-        listener.eventuallyExpect(InterfaceStateChanged(iface.name,
-                STATE_LINK_UP, ROLE_SERVER, /* IpConfiguration */ null))
+        // TODO(b/295146844): do not report IpConfiguration for server mode interfaces.
+        listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_SERVER)
 
         releaseTetheredInterface()
         listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_CLIENT)
@@ -668,6 +663,20 @@
     }
 
     @Test
+    fun testCallbacks_afterRemovingServerModeInterface() {
+        // do not run this test if an interface that can be used for tethering already exists.
+        assumeNoInterfaceForTetheringAvailable()
+
+        val iface = createInterface()
+        requestTetheredInterface().expectOnAvailable()
+        removeInterface(iface)
+
+        val listener = EthernetStateListener()
+        addInterfaceStateListener(listener)
+        listener.assertNoCallback()
+    }
+
+    @Test
     fun testGetInterfaceList() {
         // Create two test interfaces and check the return list contains the interface names.
         val iface1 = createInterface()
diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
index 81834a9..805dd65 100644
--- a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
+++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
@@ -71,8 +71,6 @@
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.RecorderCallback.CallbackEntry;
-import com.android.testutils.SkipMainlinePresubmit;
-import com.android.testutils.SkipPresubmit;
 import com.android.testutils.TestableNetworkCallback;
 
 import org.bouncycastle.x509.X509V1CertificateGenerator;
@@ -642,7 +640,6 @@
     }
 
     @Test
-    @SkipMainlinePresubmit(reason = "Out of SLO flakiness")
     public void testStartStopVpnProfileV4() throws Exception {
         doTestStartStopVpnProfile(false /* testIpv6Only */, false /* requiresValidation */,
                 false /* testSessionKey */, false /* testIkeTunConnParams */);
@@ -656,14 +653,12 @@
     }
 
     @Test
-    @SkipMainlinePresubmit(reason = "Out of SLO flakiness")
     public void testStartStopVpnProfileV6() throws Exception {
         doTestStartStopVpnProfile(true /* testIpv6Only */, false /* requiresValidation */,
                 false /* testSessionKey */, false /* testIkeTunConnParams */);
     }
 
     @Test @IgnoreUpTo(SC_V2)
-    @SkipPresubmit(reason = "Out of SLO flakiness")
     public void testStartStopVpnProfileV6WithValidation() throws Exception {
         assumeTrue(TestUtils.shouldTestTApis());
         doTestStartStopVpnProfile(true /* testIpv6Only */, true /* requiresValidation */,
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index 98ea224..5937655 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -257,7 +257,7 @@
         callback: TestableNetworkCallback,
         handler: Handler
     ) {
-        mCM!!.registerBestMatchingNetworkCallback(request, callback, handler)
+        mCM.registerBestMatchingNetworkCallback(request, callback, handler)
         callbacksToCleanUp.add(callback)
     }
 
@@ -394,8 +394,8 @@
                 .setLegacyExtraInfo(legacyExtraInfo).build()
         val (agent, callback) = createConnectedNetworkAgent(initialConfig = config)
         val networkInfo = mCM.getNetworkInfo(agent.network)
-        assertEquals(subtypeLTE, networkInfo.getSubtype())
-        assertEquals(subtypeNameLTE, networkInfo.getSubtypeName())
+        assertEquals(subtypeLTE, networkInfo?.getSubtype())
+        assertEquals(subtypeNameLTE, networkInfo?.getSubtypeName())
         assertEquals(legacyExtraInfo, config.getLegacyExtraInfo())
     }
 
@@ -417,8 +417,8 @@
             val nc = NetworkCapabilities(agent.nc)
             nc.addCapability(NET_CAPABILITY_NOT_METERED)
             agent.sendNetworkCapabilities(nc)
-            callback.expectCaps(agent.network) { it.hasCapability(NET_CAPABILITY_NOT_METERED) }
-            val networkInfo = mCM.getNetworkInfo(agent.network)
+            callback.expectCaps(agent.network!!) { it.hasCapability(NET_CAPABILITY_NOT_METERED) }
+            val networkInfo = mCM.getNetworkInfo(agent.network!!)!!
             assertEquals(subtypeUMTS, networkInfo.getSubtype())
             assertEquals(subtypeNameUMTS, networkInfo.getSubtypeName())
     }
@@ -631,6 +631,7 @@
         val defaultNetwork = mCM.activeNetwork
         assertNotNull(defaultNetwork)
         val defaultNetworkCapabilities = mCM.getNetworkCapabilities(defaultNetwork)
+        assertNotNull(defaultNetworkCapabilities)
         val defaultNetworkTransports = defaultNetworkCapabilities.transportTypes
 
         val agent = createNetworkAgent(initialNc = nc)
@@ -671,7 +672,7 @@
         // This is not very accurate because the test does not control the capabilities of the
         // underlying networks, and because not congested, not roaming, and not suspended are the
         // default anyway. It's still useful as an extra check though.
-        vpnNc = mCM.getNetworkCapabilities(agent.network!!)
+        vpnNc = mCM.getNetworkCapabilities(agent.network!!)!!
         for (cap in listOf(NET_CAPABILITY_NOT_CONGESTED,
                 NET_CAPABILITY_NOT_ROAMING,
                 NET_CAPABILITY_NOT_SUSPENDED)) {
@@ -1041,8 +1042,8 @@
     }
 
     fun QosSocketInfo(agent: NetworkAgent, socket: Closeable) = when (socket) {
-        is Socket -> QosSocketInfo(agent.network, socket)
-        is DatagramSocket -> QosSocketInfo(agent.network, socket)
+        is Socket -> QosSocketInfo(checkNotNull(agent.network), socket)
+        is DatagramSocket -> QosSocketInfo(checkNotNull(agent.network), socket)
         else -> fail("unexpected socket type")
     }
 
@@ -1405,8 +1406,8 @@
         val nc = makeTestNetworkCapabilities(ifName, transports).also {
             if (transports.contains(TRANSPORT_VPN)) {
                 val sessionId = "NetworkAgentTest-${Process.myPid()}"
-                it.transportInfo = VpnTransportInfo(VpnManager.TYPE_VPN_PLATFORM, sessionId,
-                    /*bypassable=*/ false, /*longLivedTcpConnectionsExpensive=*/ false)
+                it.setTransportInfo(VpnTransportInfo(VpnManager.TYPE_VPN_PLATFORM, sessionId,
+                    /*bypassable=*/ false, /*longLivedTcpConnectionsExpensive=*/ false))
                 it.underlyingNetworks = listOf()
             }
         }
diff --git a/tests/cts/net/src/android/net/cts/NetworkInfoTest.kt b/tests/cts/net/src/android/net/cts/NetworkInfoTest.kt
index d6120f8..499d97f 100644
--- a/tests/cts/net/src/android/net/cts/NetworkInfoTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkInfoTest.kt
@@ -16,12 +16,12 @@
 
 package android.net.cts
 
-import android.os.Build
 import android.content.Context
 import android.net.ConnectivityManager
 import android.net.NetworkInfo
 import android.net.NetworkInfo.DetailedState
 import android.net.NetworkInfo.State
+import android.os.Build
 import android.telephony.TelephonyManager
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry
@@ -29,16 +29,17 @@
 import com.android.modules.utils.build.SdkLevel
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.NonNullTestUtils
+import kotlin.reflect.jvm.isAccessible
+import kotlin.test.assertFails
+import kotlin.test.assertFailsWith
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertNotNull
 import org.junit.Assert.assertNull
 import org.junit.Assert.assertTrue
 import org.junit.Rule
-import org.junit.runner.RunWith
 import org.junit.Test
-import kotlin.reflect.jvm.isAccessible
-import kotlin.test.assertFails
-import kotlin.test.assertFailsWith
+import org.junit.runner.RunWith
 
 const val TYPE_MOBILE = ConnectivityManager.TYPE_MOBILE
 const val TYPE_WIFI = ConnectivityManager.TYPE_WIFI
@@ -106,10 +107,12 @@
         }
 
         if (SdkLevel.isAtLeastT()) {
-            assertFailsWith<NullPointerException> { NetworkInfo(null) }
+            assertFailsWith<NullPointerException> {
+                NetworkInfo(NonNullTestUtils.nullUnsafe<NetworkInfo>(null))
+            }
         } else {
             // Doesn't immediately crash on S-
-            NetworkInfo(null)
+            NetworkInfo(NonNullTestUtils.nullUnsafe<NetworkInfo>(null))
         }
     }
 
@@ -134,10 +137,11 @@
         val incorrectDetailedState = constructor.newInstance("any", 200) as DetailedState
         if (SdkLevel.isAtLeastT()) {
             assertFailsWith<NullPointerException> {
-                NetworkInfo(null)
+                NetworkInfo(NonNullTestUtils.nullUnsafe<NetworkInfo>(null))
             }
             assertFailsWith<NullPointerException> {
-                networkInfo.setDetailedState(null, "reason", "extraInfo")
+                networkInfo.setDetailedState(NonNullTestUtils.nullUnsafe<DetailedState>(null),
+                        "reason", "extraInfo")
             }
             // This actually throws ArrayOutOfBoundsException because of the implementation of
             // EnumMap, but that's an implementation detail so accept any crash.
@@ -146,8 +150,9 @@
             }
         } else {
             // Doesn't immediately crash on S-
-            NetworkInfo(null)
-            networkInfo.setDetailedState(null, "reason", "extraInfo")
+            NetworkInfo(NonNullTestUtils.nullUnsafe<NetworkInfo>(null))
+            networkInfo.setDetailedState(NonNullTestUtils.nullUnsafe<DetailedState>(null),
+                    "reason", "extraInfo")
         }
     }
-}
\ No newline at end of file
+}
diff --git a/tests/cts/net/src/android/net/cts/NetworkScoreTest.kt b/tests/cts/net/src/android/net/cts/NetworkScoreTest.kt
index 2704dd3..e660b1e 100644
--- a/tests/cts/net/src/android/net/cts/NetworkScoreTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkScoreTest.kt
@@ -67,7 +67,7 @@
 @RunWith(DevSdkIgnoreRunner::class)
 class NetworkScoreTest {
     private val TAG = javaClass.simpleName
-    private val mCm = testContext.getSystemService(ConnectivityManager::class.java)
+    private val mCm = testContext.getSystemService(ConnectivityManager::class.java)!!
     private val mHandlerThread = HandlerThread("$TAG handler thread")
     private val mHandler by lazy { Handler(mHandlerThread.looper) }
     private val agentsToCleanUp = Collections.synchronizedList(mutableListOf<NetworkAgent>())
@@ -111,7 +111,7 @@
     // made for ConnectivityServiceTest.
     // TODO : have TestNetworkCallback work for NetworkAgent too and remove this class.
     private class AgentWrapper(val agent: NetworkAgent) : HasNetwork {
-        override val network = agent.network
+        override val network = checkNotNull(agent.network)
         fun sendNetworkScore(s: NetworkScore) = agent.sendNetworkScore(s)
     }
 
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index e4ee8de..49a6ef1 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -25,6 +25,7 @@
 import android.net.LinkProperties
 import android.net.LocalSocket
 import android.net.LocalSocketAddress
+import android.net.MacAddress
 import android.net.Network
 import android.net.NetworkAgentConfig
 import android.net.NetworkCapabilities
@@ -73,6 +74,8 @@
 import android.system.OsConstants.AF_INET6
 import android.system.OsConstants.EADDRNOTAVAIL
 import android.system.OsConstants.ENETUNREACH
+import android.system.OsConstants.ETH_P_IPV6
+import android.system.OsConstants.IPPROTO_IPV6
 import android.system.OsConstants.IPPROTO_UDP
 import android.system.OsConstants.SOCK_DGRAM
 import android.util.Log
@@ -82,13 +85,21 @@
 import com.android.compatibility.common.util.PropertyUtil
 import com.android.modules.utils.build.SdkLevel.isAtLeastU
 import com.android.net.module.util.ArrayTrackRecord
+import com.android.net.module.util.DnsPacket
+import com.android.net.module.util.HexDump
+import com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.UDP_HEADER_LEN
+import com.android.net.module.util.PacketBuilder
 import com.android.net.module.util.TrackRecord
 import com.android.testutils.ConnectivityModuleTest
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.IPv6UdpFilter
 import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
 import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.TapPacketReader
 import com.android.testutils.TestableNetworkAgent
 import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkCreated
 import com.android.testutils.TestableNetworkCallback
@@ -103,6 +114,7 @@
 import java.net.InetAddress
 import java.net.NetworkInterface
 import java.net.ServerSocket
+import java.nio.ByteBuffer
 import java.nio.charset.StandardCharsets
 import java.util.Random
 import java.util.concurrent.Executor
@@ -133,6 +145,8 @@
 private const val REGISTRATION_TIMEOUT_MS = 10_000L
 private const val DBG = false
 private const val TEST_PORT = 12345
+private const val MDNS_PORT = 5353.toShort()
+private val multicastIpv6Addr = parseNumericAddress("ff02::fb") as Inet6Address
 
 @AppModeFull(reason = "Socket cannot bind in instant app mode")
 @RunWith(DevSdkIgnoreRunner::class)
@@ -145,9 +159,9 @@
     val ignoreRule = DevSdkIgnoreRule()
 
     private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
-    private val nsdManager by lazy { context.getSystemService(NsdManager::class.java) }
+    private val nsdManager by lazy { context.getSystemService(NsdManager::class.java)!! }
 
-    private val cm by lazy { context.getSystemService(ConnectivityManager::class.java) }
+    private val cm by lazy { context.getSystemService(ConnectivityManager::class.java)!! }
     private val serviceName = "NsdTest%09d".format(Random().nextInt(1_000_000_000))
     private val serviceType = "_nmt%09d._tcp".format(Random().nextInt(1_000_000_000))
     private val handlerThread = HandlerThread(NsdManagerTest::class.java.simpleName)
@@ -194,8 +208,8 @@
 
         inline fun <reified V : NsdEvent> expectCallback(timeoutMs: Long = TIMEOUT_MS): V {
             val nextEvent = nextEvents.poll(timeoutMs)
-            assertNotNull(nextEvent, "No callback received after $timeoutMs ms, expected " +
-                    "${V::class.java.simpleName}")
+            assertNotNull(nextEvent, "No callback received after $timeoutMs ms, " +
+                    "expected ${V::class.java.simpleName}")
             assertTrue(nextEvent is V, "Expected ${V::class.java.simpleName} but got " +
                     nextEvent.javaClass.simpleName)
             return nextEvent
@@ -383,7 +397,7 @@
     }
 
     private fun createTestNetwork(): TestTapNetwork {
-        val tnm = context.getSystemService(TestNetworkManager::class.java)
+        val tnm = context.getSystemService(TestNetworkManager::class.java)!!
         val iface = tnm.createTapInterface()
         val cb = TestableNetworkCallback()
         val testNetworkSpecifier = TestNetworkSpecifier(iface.interfaceName)
@@ -411,7 +425,6 @@
         val lp = LinkProperties().apply {
             interfaceName = ifaceName
         }
-
         val agent = TestableNetworkAgent(context, handlerThread.looper,
                 NetworkCapabilities().apply {
                     removeCapability(NET_CAPABILITY_TRUSTED)
@@ -1144,6 +1157,176 @@
         }
     }
 
+    @Test
+    fun testRegisterWithConflictDuringProbing() {
+        // This test requires shims supporting T+ APIs (NsdServiceInfo.network)
+        assumeTrue(TestUtils.shouldTestTApis())
+
+        val si = NsdServiceInfo()
+        si.serviceType = serviceType
+        si.serviceName = serviceName
+        si.network = testNetwork1.network
+        si.port = 12345 // Test won't try to connect so port does not matter
+
+        val packetReader = TapPacketReader(Handler(handlerThread.looper),
+                testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+        packetReader.startAsyncForTest()
+        handlerThread.waitForIdle(TIMEOUT_MS)
+
+        // Register service on testNetwork1
+        val registrationRecord = NsdRegistrationRecord()
+        nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, { it.run() },
+                registrationRecord)
+
+        tryTest {
+            assertNotNull(packetReader.pollForProbe(serviceName, serviceType),
+                    "Did not find a probe for the service")
+            packetReader.sendResponse(buildConflictingAnnouncement())
+
+            // Registration must use an updated name to avoid the conflict
+            val cb = registrationRecord.expectCallback<ServiceRegistered>(REGISTRATION_TIMEOUT_MS)
+            cb.serviceInfo.serviceName.let {
+                assertTrue("Unexpected registered name: $it",
+                        it.startsWith(serviceName) && it != serviceName)
+            }
+        } cleanupStep {
+            nsdManager.unregisterService(registrationRecord)
+            registrationRecord.expectCallback<ServiceUnregistered>()
+        } cleanup {
+            packetReader.handler.post { packetReader.stop() }
+            handlerThread.waitForIdle(TIMEOUT_MS)
+        }
+    }
+
+    @Test
+    fun testRegisterWithConflictAfterProbing() {
+        // This test requires shims supporting T+ APIs (NsdServiceInfo.network)
+        assumeTrue(TestUtils.shouldTestTApis())
+
+        val si = NsdServiceInfo()
+        si.serviceType = serviceType
+        si.serviceName = serviceName
+        si.network = testNetwork1.network
+        si.port = 12345 // Test won't try to connect so port does not matter
+
+        // Register service on testNetwork1
+        val registrationRecord = NsdRegistrationRecord()
+        val discoveryRecord = NsdDiscoveryRecord()
+        val registeredService = registerService(registrationRecord, si)
+        val packetReader = TapPacketReader(Handler(handlerThread.looper),
+                testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+        packetReader.startAsyncForTest()
+        handlerThread.waitForIdle(TIMEOUT_MS)
+
+        tryTest {
+            assertNotNull(packetReader.pollForAdvertisement(serviceName, serviceType),
+                    "No announcements sent after initial probing")
+
+            assertEquals(si.serviceName, registeredService.serviceName)
+
+            nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+                testNetwork1.network, { it.run() }, discoveryRecord)
+            discoveryRecord.waitForServiceDiscovered(si.serviceName, serviceType)
+
+            // Send a conflicting announcement
+            val conflictingAnnouncement = buildConflictingAnnouncement()
+            packetReader.sendResponse(conflictingAnnouncement)
+
+            // Expect to see probes (RFC6762 9., service is reset to probing state)
+            assertNotNull(packetReader.pollForProbe(serviceName, serviceType),
+                    "Probe not received within timeout after conflict")
+
+            // Send the conflicting packet again to reply to the probe
+            packetReader.sendResponse(conflictingAnnouncement)
+
+            // Note the legacy mdnsresponder would send an exit announcement here (a 0-lifetime
+            // advertisement just for the PTR record), but not the new advertiser. This probably
+            // follows RFC 6762 8.4, saying that when a record rdata changed, "In the case of shared
+            // records, a host MUST send a "goodbye" announcement with RR TTL zero [...] for the old
+            // rdata, to cause it to be deleted from peer caches, before announcing the new rdata".
+            //
+            // This should be implemented by the new advertiser, but in the case of conflicts it is
+            // not very valuable since an identical PTR record would be used by the conflicting
+            // service (except for subtypes). In that case the exit announcement may be
+            // counter-productive as it conflicts with announcements done by the conflicting
+            // service.
+
+            // Note that before sending the following ServiceRegistered callback for the renamed
+            // service, the legacy mdnsresponder-based implementation would first send a
+            // Service*Registered* callback for the original service name being *unregistered*; it
+            // should have been a ServiceUnregistered callback instead (bug in NsdService
+            // interpretation of the callback).
+            val newRegistration = registrationRecord.expectCallbackEventually<ServiceRegistered>(
+                    REGISTRATION_TIMEOUT_MS) {
+                it.serviceInfo.serviceName.startsWith(serviceName) &&
+                        it.serviceInfo.serviceName != serviceName
+            }
+
+            discoveryRecord.expectCallbackEventually<ServiceFound> {
+                it.serviceInfo.serviceName == newRegistration.serviceInfo.serviceName
+            }
+        } cleanupStep {
+            nsdManager.stopServiceDiscovery(discoveryRecord)
+            discoveryRecord.expectCallback<DiscoveryStopped>()
+        } cleanupStep {
+            nsdManager.unregisterService(registrationRecord)
+            registrationRecord.expectCallback<ServiceUnregistered>()
+        } cleanup {
+            packetReader.handler.post { packetReader.stop() }
+            handlerThread.waitForIdle(TIMEOUT_MS)
+        }
+    }
+
+    private fun buildConflictingAnnouncement(): ByteBuffer {
+        /*
+        Generated with:
+        scapy.raw(scapy.DNS(rd=0, qr=1, aa=1, qd = None, an =
+                scapy.DNSRRSRV(rrname='NsdTest123456789._nmt123456789._tcp.local',
+                    rclass=0x8001, port=31234, target='conflict.local', ttl=120)
+        )).hex()
+         */
+        val mdnsPayload = HexDump.hexStringToByteArray("000084000000000100000000104e736454657" +
+                "3743132333435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00002" +
+                "18001000000780016000000007a0208636f6e666c696374056c6f63616c00")
+        val packetBuffer = ByteBuffer.wrap(mdnsPayload)
+        // Replace service name and types in the packet with the random ones used in the test.
+        // Test service name and types have consistent length and are always ASCII
+        val testPacketName = "NsdTest123456789".encodeToByteArray()
+        val testPacketTypePrefix = "_nmt123456789".encodeToByteArray()
+        val encodedServiceName = serviceName.encodeToByteArray()
+        val encodedTypePrefix = serviceType.split('.')[0].encodeToByteArray()
+        assertEquals(testPacketName.size, encodedServiceName.size)
+        assertEquals(testPacketTypePrefix.size, encodedTypePrefix.size)
+        packetBuffer.position(mdnsPayload.indexOf(testPacketName))
+        packetBuffer.put(encodedServiceName)
+        packetBuffer.position(mdnsPayload.indexOf(testPacketTypePrefix))
+        packetBuffer.put(encodedTypePrefix)
+
+        return buildMdnsPacket(mdnsPayload)
+    }
+
+    private fun buildMdnsPacket(mdnsPayload: ByteArray): ByteBuffer {
+        val packetBuffer = PacketBuilder.allocate(true /* hasEther */, IPPROTO_IPV6,
+                IPPROTO_UDP, mdnsPayload.size)
+        val packetBuilder = PacketBuilder(packetBuffer)
+        // Multicast ethernet address for IPv6 to ff02::fb
+        val multicastEthAddr = MacAddress.fromBytes(
+                byteArrayOf(0x33, 0x33, 0, 0, 0, 0xfb.toByte()))
+        packetBuilder.writeL2Header(
+                MacAddress.fromBytes(byteArrayOf(1, 2, 3, 4, 5, 6)) /* srcMac */,
+                multicastEthAddr,
+                ETH_P_IPV6.toShort())
+        packetBuilder.writeIpv6Header(
+                0x60000000, // version=6, traffic class=0x0, flowlabel=0x0
+                IPPROTO_UDP.toByte(),
+                64 /* hop limit */,
+                parseNumericAddress("2001:db8::123") as Inet6Address /* srcIp */,
+                multicastIpv6Addr /* dstIp */)
+        packetBuilder.writeUdpHeader(MDNS_PORT /* srcPort */, MDNS_PORT /* dstPort */)
+        packetBuffer.put(mdnsPayload)
+        return packetBuilder.finalizePacket()
+    }
+
     /**
      * Register a service and return its registration record.
      */
@@ -1169,7 +1352,65 @@
     }
 }
 
+private fun TapPacketReader.pollForMdnsPacket(
+    timeoutMs: Long = REGISTRATION_TIMEOUT_MS,
+    predicate: (TestDnsPacket) -> Boolean
+): ByteArray? {
+    val mdnsProbeFilter = IPv6UdpFilter(srcPort = MDNS_PORT, dstPort = MDNS_PORT).and {
+        val mdnsPayload = it.copyOfRange(
+                ETHER_HEADER_LEN + IPV6_HEADER_LEN + UDP_HEADER_LEN, it.size)
+        try {
+            predicate(TestDnsPacket(mdnsPayload))
+        } catch (e: DnsPacket.ParseException) {
+            false
+        }
+    }
+    return poll(timeoutMs, mdnsProbeFilter)
+}
+
+private fun TapPacketReader.pollForProbe(
+    serviceName: String,
+    serviceType: String,
+    timeoutMs: Long = REGISTRATION_TIMEOUT_MS
+): ByteArray? = pollForMdnsPacket(timeoutMs) { it.isProbeFor("$serviceName.$serviceType.local") }
+
+private fun TapPacketReader.pollForAdvertisement(
+    serviceName: String,
+    serviceType: String,
+    timeoutMs: Long = REGISTRATION_TIMEOUT_MS
+): ByteArray? = pollForMdnsPacket(timeoutMs) { it.isReplyFor("$serviceName.$serviceType.local") }
+
+private class TestDnsPacket(data: ByteArray) : DnsPacket(data) {
+    fun isProbeFor(name: String): Boolean = mRecords[QDSECTION].any {
+        it.dName == name && it.nsType == 0xff /* ANY */
+    }
+
+    fun isReplyFor(name: String): Boolean = mRecords[ANSECTION].any {
+        it.dName == name && it.nsType == 0x21 /* SRV */
+    }
+}
+
 private fun ByteArray?.utf8ToString(): String {
     if (this == null) return ""
     return String(this, StandardCharsets.UTF_8)
 }
+
+private fun ByteArray.indexOf(sub: ByteArray): Int {
+    var subIndex = 0
+    forEachIndexed { i, b ->
+        when (b) {
+            // Still matching: continue comparing with next byte
+            sub[subIndex] -> {
+                subIndex++
+                if (subIndex == sub.size) {
+                    return i - sub.size + 1
+                }
+            }
+            // Not matching next byte but matches first byte: continue comparing with 2nd byte
+            sub[0] -> subIndex = 1
+            // No matches: continue comparing from first byte
+            else -> subIndex = 0
+        }
+    }
+    return -1
+}
diff --git a/tests/cts/net/src/android/net/cts/ProxyTest.kt b/tests/cts/net/src/android/net/cts/ProxyTest.kt
index a661b26..872dbb9 100644
--- a/tests/cts/net/src/android/net/cts/ProxyTest.kt
+++ b/tests/cts/net/src/android/net/cts/ProxyTest.kt
@@ -70,7 +70,7 @@
 
     private fun getDefaultProxy(): ProxyInfo? {
         return InstrumentationRegistry.getInstrumentation().context
-                .getSystemService(ConnectivityManager::class.java)
+                .getSystemService(ConnectivityManager::class.java)!!
                 .getDefaultProxy()
     }
 
@@ -100,4 +100,4 @@
             Proxy.setHttpProxyConfiguration(original)
         }
     }
-}
\ No newline at end of file
+}
diff --git a/tests/cts/net/src/android/net/cts/TestNetworkRunnable.java b/tests/cts/net/src/android/net/cts/TestNetworkRunnable.java
index 0eb5644..1b22f42 100644
--- a/tests/cts/net/src/android/net/cts/TestNetworkRunnable.java
+++ b/tests/cts/net/src/android/net/cts/TestNetworkRunnable.java
@@ -95,14 +95,17 @@
                 testIface.getFileDescriptor().close();
             }
 
-            if (tunNetworkCallback != null) {
-                sCm.unregisterNetworkCallback(tunNetworkCallback);
-            }
 
             final Network testNetwork = tunNetworkCallback.currentNetwork;
             if (testNetwork != null) {
                 tnm.teardownTestNetwork(testNetwork);
             }
+            // Ensure test network being torn down.
+            tunNetworkCallback.waitForLost();
+
+            if (tunNetworkCallback != null) {
+                sCm.unregisterNetworkCallback(tunNetworkCallback);
+            }
         }
     }
 
diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
index 21f1358..aa09b84 100644
--- a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
+++ b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
@@ -16,6 +16,7 @@
 
 package android.net.cts.util;
 
+import static android.Manifest.permission.MODIFY_PHONE_STATE;
 import static android.Manifest.permission.NETWORK_SETTINGS;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
@@ -54,6 +55,8 @@
 import android.os.IBinder;
 import android.system.Os;
 import android.system.OsConstants;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.util.Log;
 
@@ -567,6 +570,42 @@
     }
 
     /**
+     * Enables or disables the mobile data and waits for the state to change.
+     *
+     * @param enabled - true to enable, false to disable the mobile data.
+     */
+    public void setMobileDataEnabled(boolean enabled) throws InterruptedException {
+        final TelephonyManager tm =  mContext.getSystemService(TelephonyManager.class)
+                .createForSubscriptionId(SubscriptionManager.getDefaultDataSubscriptionId());
+        final NetworkRequest request = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .build();
+        final TestNetworkCallback callback = new TestNetworkCallback();
+        mCm.requestNetwork(request, callback);
+
+        try {
+            if (!enabled) {
+                assertNotNull("Cannot disable mobile data unless mobile data is connected",
+                        callback.waitForAvailable());
+            }
+
+            runAsShell(MODIFY_PHONE_STATE, () -> tm.setDataEnabledForReason(
+                    TelephonyManager.DATA_ENABLED_REASON_USER, enabled));
+            if (enabled) {
+                assertNotNull("Enabling mobile data did not connect mobile data",
+                        callback.waitForAvailable());
+            } else {
+                assertNotNull("Disabling mobile data did not disconnect mobile data",
+                        callback.waitForLost());
+            }
+
+        } finally {
+            mCm.unregisterNetworkCallback(callback);
+        }
+    }
+
+    /**
      * Receiver that captures the last connectivity change's network type and state. Recognizes
      * both {@code CONNECTIVITY_ACTION} and {@code NETWORK_CALLBACK_ACTION} intents.
      */
diff --git a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
index 67e1296..e264b55 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
@@ -56,6 +56,7 @@
 import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
 import com.android.testutils.TestableNetworkCallback
 import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
 import kotlin.test.assertTrue
 import kotlin.test.fail
 import org.junit.After
@@ -291,6 +292,7 @@
         val capportData = testCb.expect<LinkPropertiesChanged>(na, TEST_TIMEOUT_MS) {
             it.lp.captivePortalData != null
         }.lp.captivePortalData
+        assertNotNull(capportData)
         assertTrue(capportData.isCaptive)
         assertEquals(Uri.parse("https://login.capport.android.com"), capportData.userPortalUrl)
         assertEquals(Uri.parse("https://venueinfo.capport.android.com"), capportData.venueInfoUrl)
diff --git a/tests/integration/src/com/android/server/net/integrationtests/HttpResponse.kt b/tests/integration/src/com/android/server/net/integrationtests/HttpResponse.kt
index e206313..467708a 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/HttpResponse.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/HttpResponse.kt
@@ -20,9 +20,9 @@
 import android.os.Parcelable
 
 data class HttpResponse(
-    val requestUrl: String,
+    val requestUrl: String?,
     val responseCode: Int,
-    val content: String = "",
+    val content: String? = "",
     val redirectUrl: String? = null
 ) : Parcelable {
     constructor(p: Parcel): this(p.readString(), p.readInt(), p.readString(), p.readString())
@@ -46,4 +46,4 @@
         override fun createFromParcel(source: Parcel) = HttpResponse(source)
         override fun newArray(size: Int) = arrayOfNulls<HttpResponse?>(size)
     }
-}
\ No newline at end of file
+}
diff --git a/tests/integration/src/com/android/server/net/integrationtests/NetworkStackInstrumentationService.kt b/tests/integration/src/com/android/server/net/integrationtests/NetworkStackInstrumentationService.kt
index e807952..104d063 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/NetworkStackInstrumentationService.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/NetworkStackInstrumentationService.kt
@@ -70,7 +70,7 @@
          * request is seen, the test will fail.
          */
         override fun addHttpResponse(response: HttpResponse) {
-            httpResponses.getValue(response.requestUrl).add(response)
+            httpResponses.getValue(checkNotNull(response.requestUrl)).add(response)
         }
 
         /**
@@ -81,4 +81,4 @@
             return ArrayList(httpRequestUrls)
         }
     }
-}
\ No newline at end of file
+}
diff --git a/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt b/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt
index 361c968..7e227c4 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt
@@ -69,7 +69,8 @@
         url: URL,
         private val response: HttpResponse
     ) : HttpURLConnection(url) {
-        private val responseBytes = response.content.toByteArray(StandardCharsets.UTF_8)
+        private val responseBytes = checkNotNull(response.content)
+            .toByteArray(StandardCharsets.UTF_8)
         override fun getResponseCode() = response.responseCode
         override fun getContentLengthLong() = responseBytes.size.toLong()
         override fun getHeaderField(field: String): String? {
diff --git a/tests/mts/bpf_existence_test.cpp b/tests/mts/bpf_existence_test.cpp
index c294e7b..442d69f 100644
--- a/tests/mts/bpf_existence_test.cpp
+++ b/tests/mts/bpf_existence_test.cpp
@@ -33,6 +33,8 @@
 using android::modules::sdklevel::IsAtLeastR;
 using android::modules::sdklevel::IsAtLeastS;
 using android::modules::sdklevel::IsAtLeastT;
+using android::modules::sdklevel::IsAtLeastU;
+using android::modules::sdklevel::IsAtLeastV;
 
 #define PLATFORM "/sys/fs/bpf/"
 #define TETHERING "/sys/fs/bpf/tethering/"
@@ -147,10 +149,14 @@
     // so we should only test for the removal of stuff that was mainline'd,
     // and for the presence of mainline stuff.
 
+    // Note: Q is no longer supported by mainline
+    ASSERT_TRUE(IsAtLeastR());
+
     // R can potentially run on pre-4.9 kernel non-eBPF capable devices.
     DO_EXPECT(IsAtLeastR() && !IsAtLeastS() && isAtLeastKernelVersion(4, 9, 0), PLATFORM_ONLY_IN_R);
 
     // S requires Linux Kernel 4.9+ and thus requires eBPF support.
+    if (IsAtLeastS()) ASSERT_TRUE(isAtLeastKernelVersion(4, 9, 0));
     DO_EXPECT(IsAtLeastS(), MAINLINE_FOR_S_PLUS);
     DO_EXPECT(IsAtLeastS() && isAtLeastKernelVersion(5, 10, 0), MAINLINE_FOR_S_5_10_PLUS);
 
@@ -163,6 +169,10 @@
     DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(5, 15, 0), MAINLINE_FOR_T_5_15_PLUS);
 
     // U requires Linux Kernel 4.14+, but nothing (as yet) added or removed in U.
+    if (IsAtLeastU()) ASSERT_TRUE(isAtLeastKernelVersion(4, 14, 0));
+
+    // V requires Linux Kernel 4.19+, but nothing (as yet) added or removed in V.
+    if (IsAtLeastV()) ASSERT_TRUE(isAtLeastKernelVersion(4, 19, 0));
 
     for (const auto& file : mustExist) {
         EXPECT_EQ(0, access(file.c_str(), R_OK)) << file << " does not exist";
diff --git a/tests/unit/java/android/net/NetworkTemplateTest.kt b/tests/unit/java/android/net/NetworkTemplateTest.kt
index 2f6c76b..a8414ca 100644
--- a/tests/unit/java/android/net/NetworkTemplateTest.kt
+++ b/tests/unit/java/android/net/NetworkTemplateTest.kt
@@ -49,6 +49,7 @@
 import android.telephony.TelephonyManager
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.NonNullTestUtils
 import com.android.testutils.assertParcelSane
 import kotlin.test.assertEquals
 import kotlin.test.assertFalse
@@ -221,12 +222,13 @@
     @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.TIRAMISU)
     @Test
     fun testBuildTemplateMobileAll_nullSubscriberId() {
-        val templateMobileAllWithNullImsi = buildTemplateMobileAll(null)
+        val templateMobileAllWithNullImsi =
+                buildTemplateMobileAll(NonNullTestUtils.nullUnsafe<String>(null))
         val setWithNull = HashSet<String?>().apply {
             add(null)
         }
         val templateFromBuilder = NetworkTemplate.Builder(MATCH_MOBILE).setMeteredness(METERED_YES)
-            .setSubscriberIds(setWithNull).build()
+                .setSubscriberIds(setWithNull).build()
         assertEquals(templateFromBuilder, templateMobileAllWithNullImsi)
     }
 
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 3688d83..708697c 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -2301,7 +2301,7 @@
         }
 
         @Override
-        public boolean isFeatureEnabled(@NonNull final String name, final boolean defaultEnabled) {
+        public boolean isTetheringFeatureNotChickenedOut(@NonNull final String name) {
             // Tests for enabling the feature are verified in AutomaticOnOffKeepaliveTrackerTest.
             // Assuming enabled here to focus on ConnectivityService tests.
             return true;
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 7a6424e..f0c7dcc 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -236,8 +236,8 @@
     @After
     public void tearDown() throws Exception {
         if (mThread != null) {
-            mThread.quit();
-            mThread = null;
+            mThread.quitSafely();
+            mThread.join();
         }
     }
 
diff --git a/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
index b69b042..986c389 100644
--- a/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
@@ -32,7 +32,6 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.longThat;
@@ -359,7 +358,7 @@
                 .when(mDependencies)
                 .newKeepaliveStatsTracker(mCtx, mTestHandler);
 
-        doReturn(true).when(mDependencies).isFeatureEnabled(any(), anyBoolean());
+        doReturn(true).when(mDependencies).isTetheringFeatureNotChickenedOut(any());
         doReturn(0L).when(mDependencies).getElapsedRealtime();
         mAOOKeepaliveTracker =
                 new AutomaticOnOffKeepaliveTracker(mCtx, mTestHandler, mDependencies);
@@ -368,6 +367,10 @@
     @After
     public void teardown() throws Exception {
         TestKeepaliveInfo.closeAllSockets();
+        if (mHandlerThread != null) {
+            mHandlerThread.quitSafely();
+            mHandlerThread.join();
+        }
     }
 
     private final class AOOTestHandler extends Handler {
diff --git a/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt b/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt
index 3520c5b..0a3822a 100644
--- a/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt
@@ -74,7 +74,7 @@
 
     private val TAG = this::class.simpleName
 
-    private var wtfHandler: Log.TerribleFailureHandler? = null
+    private lateinit var wtfHandler: Log.TerribleFailureHandler
 
     @Before
     fun setUp() {
diff --git a/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java b/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java
index fa703eb..90a0edd 100644
--- a/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java
@@ -67,6 +67,7 @@
 import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.HandlerUtils;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -240,6 +241,14 @@
         HandlerUtils.waitForIdle(mTestHandler, TIMEOUT_MS);
     }
 
+    @After
+    public void tearDown() throws Exception {
+        if (mHandlerThread != null) {
+            mHandlerThread.quitSafely();
+            mHandlerThread.join();
+        }
+    }
+
     private void setElapsedRealtime(long time) {
         doReturn(time).when(mDependencies).getElapsedRealtime();
     }
diff --git a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
index 8dcfffa..5bde31a 100644
--- a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
@@ -100,6 +100,7 @@
 import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.HandlerUtils;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -214,6 +215,14 @@
         onUserAdded(MOCK_USER1);
     }
 
+    @After
+    public void tearDown() throws Exception {
+        if (mHandlerThread != null) {
+            mHandlerThread.quitSafely();
+            mHandlerThread.join();
+        }
+    }
+
     private boolean hasRestrictedNetworkPermission(String partition, int targetSdkVersion,
             String packageName, int uid, String... permissions) {
         final PackageInfo packageInfo =
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketTest.kt
index f88da1f..b667e5f 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketTest.kt
@@ -59,12 +59,12 @@
         }
 
         assertEquals(InetAddresses.parseNumericAddress("192.0.2.123"),
-                (packet.authorityRecords[0] as MdnsInetAddressRecord).inet4Address)
+                (packet.authorityRecords[0] as MdnsInetAddressRecord).inet4Address!!)
         assertEquals(InetAddresses.parseNumericAddress("2001:db8::123"),
-                (packet.authorityRecords[1] as MdnsInetAddressRecord).inet6Address)
+                (packet.authorityRecords[1] as MdnsInetAddressRecord).inet6Address!!)
         assertEquals(InetAddresses.parseNumericAddress("2001:db8::456"),
-                (packet.authorityRecords[2] as MdnsInetAddressRecord).inet6Address)
+                (packet.authorityRecords[2] as MdnsInetAddressRecord).inet6Address!!)
         assertEquals(InetAddresses.parseNumericAddress("2001:db8::789"),
-                (packet.authorityRecords[3] as MdnsInetAddressRecord).inet6Address)
+                (packet.authorityRecords[3] as MdnsInetAddressRecord).inet6Address!!)
     }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java
index c0b74e1..1cc9985 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java
@@ -78,6 +78,7 @@
 import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.HandlerUtils;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -119,6 +120,7 @@
     @Mock private NetworkInterfaceWrapper mLocalOnlyIfaceWrapper;
     @Mock private NetworkInterfaceWrapper mTetheredIfaceWrapper;
     @Mock private SocketRequestMonitor mSocketRequestMonitor;
+    private HandlerThread mHandlerThread;
     private Handler mHandler;
     private MdnsSocketProvider mSocketProvider;
     private NetworkCallback mNetworkCallback;
@@ -157,9 +159,9 @@
                 eq(TETHERED_IFACE_NAME), any());
         doReturn(789).when(mDeps).getNetworkInterfaceIndexByName(
                 eq(WIFI_P2P_IFACE_NAME), any());
-        final HandlerThread thread = new HandlerThread("MdnsSocketProviderTest");
-        thread.start();
-        mHandler = new Handler(thread.getLooper());
+        mHandlerThread = new HandlerThread("MdnsSocketProviderTest");
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
 
         doReturn(mTestSocketNetLinkMonitor).when(mDeps).createSocketNetlinkMonitor(any(), any(),
                 any());
@@ -170,10 +172,18 @@
             return mTestSocketNetLinkMonitor;
         }).when(mDeps).createSocketNetlinkMonitor(any(), any(),
                 any());
-        mSocketProvider = new MdnsSocketProvider(mContext, thread.getLooper(), mDeps, mLog,
+        mSocketProvider = new MdnsSocketProvider(mContext, mHandlerThread.getLooper(), mDeps, mLog,
                 mSocketRequestMonitor);
     }
 
+    @After
+    public void tearDown() throws Exception {
+        if (mHandlerThread != null) {
+            mHandlerThread.quitSafely();
+            mHandlerThread.join();
+        }
+    }
+
     private void runOnHandler(Runnable r) {
         mHandler.post(r);
         HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
diff --git a/thread/OWNERS b/thread/OWNERS
new file mode 100644
index 0000000..c93ec4d
--- /dev/null
+++ b/thread/OWNERS
@@ -0,0 +1,11 @@
+# Bug component: 1203089
+
+# Primary reviewers
+wgtdkp@google.com
+handaw@google.com
+sunytt@google.com
+
+# Secondary reviewers
+jonhui@google.com
+xyk@google.com
+zhanglongxia@google.com
diff --git a/thread/README.md b/thread/README.md
new file mode 100644
index 0000000..f50e0cd
--- /dev/null
+++ b/thread/README.md
@@ -0,0 +1,3 @@
+# Thread
+
+Bring the [Thread](https://www.threadgroup.org/) networking protocol to Android.
diff --git a/thread/framework/Android.bp b/thread/framework/Android.bp
new file mode 100644
index 0000000..cc598d8
--- /dev/null
+++ b/thread/framework/Android.bp
@@ -0,0 +1,31 @@
+//
+// Copyright (C) 2023 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+filegroup {
+    name: "framework-thread-sources",
+    srcs: [
+        "java/**/*.java",
+        "java/**/*.aidl",
+    ],
+    path: "java",
+    visibility: [
+        "//packages/modules/Connectivity:__subpackages__",
+    ],
+}
diff --git a/thread/service/Android.bp b/thread/service/Android.bp
new file mode 100644
index 0000000..fda206a
--- /dev/null
+++ b/thread/service/Android.bp
@@ -0,0 +1,36 @@
+//
+// Copyright (C) 2023 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+filegroup {
+    name: "service-thread-sources",
+    srcs: ["java/**/*.java"],
+}
+
+java_library {
+    name: "service-thread-pre-jarjar",
+    defaults: ["framework-system-server-module-defaults"],
+    sdk_version: "system_server_current",
+    // This is included in service-connectivity which is 30+
+    // TODO (b/293613362): allow APEXes to have service jars with higher min_sdk than the APEX
+    // (service-connectivity is only used on 31+) and use 31 here
+    min_sdk_version: "30",
+    srcs: [":service-thread-sources"],
+    apex_available: ["com.android.tethering"],
+}