Merge "Revert "Ignore @FlaggedApi annotated API in udc mainline branch"" into udc-mainline-prod
diff --git a/Cronet/tools/import/copy.bara.sky b/Cronet/tools/import/copy.bara.sky
index 4a92a13..61e3ba4 100644
--- a/Cronet/tools/import/copy.bara.sky
+++ b/Cronet/tools/import/copy.bara.sky
@@ -86,6 +86,7 @@
         "third_party/brotli/**",
         # Note: Only used for tests.
         "third_party/ced/**",
+        "third_party/cpu_features/**",
         # Note: Only used for tests.
         "third_party/google_benchmark/**",
         # Note: Only used for tests.
diff --git a/TEST_MAPPING b/TEST_MAPPING
index d33453c..fafd3bb 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -1,7 +1,12 @@
 {
   "presubmit": [
     {
-      "name": "ConnectivityCoverageTests"
+      "name": "ConnectivityCoverageTests",
+      "options": [
+        {
+          "exclude-annotation": "com.android.testutils.NetworkStackModuleTest"
+        }
+      ]
     },
     {
       // In addition to ConnectivityCoverageTests, runs non-connectivity-module tests
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index f6d8070..4d92512 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -96,6 +96,7 @@
     },
     binaries: [
         "clatd",
+        "netbpfload",
         "ot-daemon",
     ],
     canned_fs_config: "canned_fs_config",
@@ -213,6 +214,7 @@
             "android.net.http.apihelpers",
             "android.net.netstats.provider",
             "android.net.nsd",
+            "android.net.thread",
             "android.net.wear",
         ],
     },
diff --git a/Tethering/apex/canned_fs_config b/Tethering/apex/canned_fs_config
index 5a03347..1f5fcfa 100644
--- a/Tethering/apex/canned_fs_config
+++ b/Tethering/apex/canned_fs_config
@@ -1,2 +1,3 @@
 /bin/for-system 0 1000 0750
 /bin/for-system/clatd 1029 1029 06755
+/bin/netbpfload 0 0 0750
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 a280046..4d1e7ef 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
@@ -167,7 +167,6 @@
 
     @Override
     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;
 
@@ -185,7 +184,6 @@
 
     @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;
 
@@ -200,8 +198,6 @@
 
     @Override
     public boolean addIpv6DownstreamRule(@NonNull final Ipv6DownstreamRule rule) {
-        if (!isInitialized()) return false;
-
         final TetherDownstream6Key key = rule.makeTetherDownstream6Key();
         final Tether6Value value = rule.makeTether6Value();
 
@@ -217,8 +213,6 @@
 
     @Override
     public boolean removeIpv6DownstreamRule(@NonNull final Ipv6DownstreamRule rule) {
-        if (!isInitialized()) return false;
-
         try {
             mBpfDownstream6Map.deleteEntry(rule.makeTetherDownstream6Key());
         } catch (ErrnoException e) {
@@ -234,8 +228,6 @@
     @Override
     @Nullable
     public SparseArray<TetherStatsValue> tetherOffloadGetStats() {
-        if (!isInitialized()) return null;
-
         final SparseArray<TetherStatsValue> tetherStatsList = new SparseArray<TetherStatsValue>();
         try {
             // The reported tether stats are total data usage for all currently-active upstream
@@ -250,8 +242,6 @@
 
     @Override
     public boolean tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes) {
-        if (!isInitialized()) return false;
-
         // The common case is an update, where the stats already exist,
         // hence we read first, even though writing with BPF_NOEXIST
         // first would make the code simpler.
@@ -307,8 +297,6 @@
     @Override
     @Nullable
     public TetherStatsValue tetherOffloadGetAndClearStats(int ifIndex) {
-        if (!isInitialized()) return null;
-
         // getAndClearTetherOffloadStats is called after all offload rules have already been
         // deleted for the given upstream interface. Before starting to do cleanup stuff in this
         // function, use synchronizeKernelRCU to make sure that all the current running eBPF
@@ -354,8 +342,6 @@
     @Override
     public boolean tetherOffloadRuleAdd(boolean downstream, @NonNull Tether4Key key,
             @NonNull Tether4Value value) {
-        if (!isInitialized()) return false;
-
         try {
             if (downstream) {
                 mBpfDownstream4Map.insertEntry(key, value);
@@ -379,8 +365,6 @@
 
     @Override
     public boolean tetherOffloadRuleRemove(boolean downstream, @NonNull Tether4Key key) {
-        if (!isInitialized()) return false;
-
         try {
             if (downstream) {
                 if (!mBpfDownstream4Map.deleteEntry(key)) return false;  // Rule did not exist
@@ -413,8 +397,6 @@
     @Override
     public void tetherOffloadRuleForEach(boolean downstream,
             @NonNull ThrowingBiConsumer<Tether4Key, Tether4Value> action) {
-        if (!isInitialized()) return;
-
         try {
             if (downstream) {
                 mBpfDownstream4Map.forEach(action);
@@ -428,8 +410,6 @@
 
     @Override
     public boolean attachProgram(String iface, boolean downstream, boolean ipv4) {
-        if (!isInitialized()) return false;
-
         try {
             BpfUtils.attachProgram(iface, downstream, ipv4);
         } catch (IOException e) {
@@ -441,8 +421,6 @@
 
     @Override
     public boolean detachProgram(String iface, boolean ipv4) {
-        if (!isInitialized()) return false;
-
         try {
             BpfUtils.detachProgram(iface, ipv4);
         } catch (IOException e) {
@@ -460,8 +438,6 @@
 
     @Override
     public boolean addDevMap(int ifIndex) {
-        if (!isInitialized()) return false;
-
         try {
             mBpfDevMap.updateEntry(new TetherDevKey(ifIndex), new TetherDevValue(ifIndex));
         } catch (ErrnoException e) {
@@ -473,8 +449,6 @@
 
     @Override
     public boolean removeDevMap(int ifIndex) {
-        if (!isInitialized()) return false;
-
         try {
             mBpfDevMap.deleteEntry(new TetherDevKey(ifIndex));
         } catch (ErrnoException e) {
diff --git a/Tethering/common/TetheringLib/api/current.txt b/Tethering/common/TetheringLib/api/current.txt
index d802177..14191eb 100644
--- a/Tethering/common/TetheringLib/api/current.txt
+++ b/Tethering/common/TetheringLib/api/current.txt
@@ -1 +1,3 @@
 // Signature format: 2.0
+// - add-additional-overrides=no
+// - migrating=Migration in progress see b/299366704
diff --git a/Tethering/common/TetheringLib/api/module-lib-current.txt b/Tethering/common/TetheringLib/api/module-lib-current.txt
index 460c216..f09b26d 100644
--- a/Tethering/common/TetheringLib/api/module-lib-current.txt
+++ b/Tethering/common/TetheringLib/api/module-lib-current.txt
@@ -1,4 +1,6 @@
 // Signature format: 2.0
+// - add-additional-overrides=no
+// - migrating=Migration in progress see b/299366704
 package android.net {
 
   public final class TetheringConstants {
diff --git a/Tethering/common/TetheringLib/api/module-lib-removed.txt b/Tethering/common/TetheringLib/api/module-lib-removed.txt
index d802177..14191eb 100644
--- a/Tethering/common/TetheringLib/api/module-lib-removed.txt
+++ b/Tethering/common/TetheringLib/api/module-lib-removed.txt
@@ -1 +1,3 @@
 // Signature format: 2.0
+// - add-additional-overrides=no
+// - migrating=Migration in progress see b/299366704
diff --git a/Tethering/common/TetheringLib/api/removed.txt b/Tethering/common/TetheringLib/api/removed.txt
index d802177..14191eb 100644
--- a/Tethering/common/TetheringLib/api/removed.txt
+++ b/Tethering/common/TetheringLib/api/removed.txt
@@ -1 +1,3 @@
 // Signature format: 2.0
+// - add-additional-overrides=no
+// - migrating=Migration in progress see b/299366704
diff --git a/Tethering/common/TetheringLib/api/system-current.txt b/Tethering/common/TetheringLib/api/system-current.txt
index 844ff64..83cee25 100644
--- a/Tethering/common/TetheringLib/api/system-current.txt
+++ b/Tethering/common/TetheringLib/api/system-current.txt
@@ -1,4 +1,6 @@
 // Signature format: 2.0
+// - add-additional-overrides=no
+// - migrating=Migration in progress see b/299366704
 package android.net {
 
   public final class TetheredClient implements android.os.Parcelable {
diff --git a/Tethering/common/TetheringLib/api/system-removed.txt b/Tethering/common/TetheringLib/api/system-removed.txt
index d802177..14191eb 100644
--- a/Tethering/common/TetheringLib/api/system-removed.txt
+++ b/Tethering/common/TetheringLib/api/system-removed.txt
@@ -1 +1,3 @@
 // Signature format: 2.0
+// - add-additional-overrides=no
+// - migrating=Migration in progress see b/299366704
diff --git a/Tethering/proguard.flags b/Tethering/proguard.flags
index 109bbda..47e2848 100644
--- a/Tethering/proguard.flags
+++ b/Tethering/proguard.flags
@@ -15,6 +15,10 @@
     native <methods>;
 }
 
+-keep class com.android.networkstack.tethering.util.TetheringUtils {
+    native <methods>;
+}
+
 # Ensure runtime-visible field annotations are kept when using R8 full mode.
 -keepattributes RuntimeVisibleAnnotations,AnnotationDefault
 -keep interface com.android.networkstack.tethering.util.Struct$Field {
diff --git a/Tethering/res/values-ky/strings.xml b/Tethering/res/values-ky/strings.xml
index 35e6453..4696a72 100644
--- a/Tethering/res/values-ky/strings.xml
+++ b/Tethering/res/values-ky/strings.xml
@@ -20,7 +20,7 @@
     <string name="tethered_notification_message" msgid="2338023450330652098">"Тууралоо үчүн басыңыз."</string>
     <string name="disable_tether_notification_title" msgid="3183576627492925522">"Модем режими өчүк"</string>
     <string name="disable_tether_notification_message" msgid="6655882039707534929">"Кеңири маалымат үчүн администраторуңузга кайрылыңыз"</string>
-    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Хотспот жана байланыш түйүнүүн статусу"</string>
+    <string name="notification_channel_tethering_status" msgid="7030733422705019001">"Байланыш түйүнү жана байланыш түйүнүүн статусу"</string>
     <string name="no_upstream_notification_title" msgid="2052743091868702475"></string>
     <string name="no_upstream_notification_message" msgid="6932020551635470134"></string>
     <string name="no_upstream_notification_disable_button" msgid="8836277213343697023"></string>
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index a851410..e030902 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -127,6 +127,7 @@
     // TODO: have this configurable
     private static final int DHCP_LEASE_TIME_SECS = 3600;
 
+    private static final int NO_UPSTREAM = 0;
     private static final MacAddress NULL_MAC_ADDRESS = MacAddress.fromString("00:00:00:00:00:00");
 
     private static final String TAG = "IpServer";
@@ -259,6 +260,12 @@
     private int mLastError;
     private int mServingMode;
     private InterfaceSet mUpstreamIfaceSet;  // may change over time
+    // mInterfaceParams can't be final now because IpServer will be created when receives
+    // WIFI_AP_STATE_CHANGED broadcasts or when it detects that the wifi interface has come up.
+    // In the latter case, the interface is not fully initialized and the MAC address might not
+    // be correct (it will be set with a randomized MAC address later).
+    // TODO: Consider create the IpServer only when tethering want to enable it, then we can
+    //       make mInterfaceParams final.
     private InterfaceParams mInterfaceParams;
     // TODO: De-duplicate this with mLinkProperties above. Currently, these link
     // properties are those selected by the IPv6TetheringCoordinator and relayed
@@ -740,7 +747,7 @@
         RaParams params = null;
         String upstreamIface = null;
         InterfaceParams upstreamIfaceParams = null;
-        int upstreamIfIndex = 0;
+        int upstreamIfIndex = NO_UPSTREAM;
 
         if (v6only != null) {
             upstreamIface = v6only.getInterfaceName();
@@ -772,7 +779,7 @@
         // CMD_TETHER_CONNECTION_CHANGED. Adding the mapping update here to the avoid potential
         // timing issue. It prevents that the IPv6 capability is updated later than
         // CMD_TETHER_CONNECTION_CHANGED.
-        mBpfCoordinator.addUpstreamNameToLookupTable(upstreamIfIndex, upstreamIface);
+        mBpfCoordinator.maybeAddUpstreamToLookupTable(upstreamIfIndex, upstreamIface);
 
         // If v6only is null, we pass in null to setRaParams(), which handles
         // deprecation of any existing RA data.
@@ -780,8 +787,7 @@
 
         // Not support BPF on virtual upstream interface
         final boolean upstreamSupportsBpf = upstreamIface != null && !isVcnInterface(upstreamIface);
-        updateIpv6ForwardingRules(
-                mLastIPv6UpstreamIfindex, upstreamIfIndex, upstreamSupportsBpf, null);
+        updateIpv6ForwardingRules(mLastIPv6UpstreamIfindex, upstreamIfIndex, upstreamSupportsBpf);
         mLastIPv6LinkProperties = v6only;
         mLastIPv6UpstreamIfindex = upstreamIfIndex;
         mUpstreamSupportsBpf = upstreamSupportsBpf;
@@ -887,25 +893,23 @@
         }
     }
 
-    // Handles all updates to IPv6 forwarding rules. These can currently change only if the upstream
-    // changes or if a neighbor event is received.
+    private int getInterfaceIndexForRule(int ifindex, boolean supportsBpf) {
+        return supportsBpf ? ifindex : NO_UPSTREAM;
+    }
+
+    // Handles updates to IPv6 forwarding rules if the upstream changes.
     private void updateIpv6ForwardingRules(int prevUpstreamIfindex, int upstreamIfindex,
-            boolean upstreamSupportsBpf, NeighborEvent e) {
-        // If no longer have an upstream or upstream not supports BPF, clear forwarding rules and do
-        // nothing else.
-        // TODO: Rather than always clear rules, ensure whether ipv6 ever enable first.
-        if (upstreamIfindex == 0 || !upstreamSupportsBpf) {
-            mBpfCoordinator.tetherOffloadRuleClear(this);
-            return;
-        }
-
+            boolean upstreamSupportsBpf) {
         // If the upstream interface has changed, remove all rules and re-add them with the new
-        // upstream interface.
+        // upstream interface. If upstream is a virtual network, treated as no upstream.
         if (prevUpstreamIfindex != upstreamIfindex) {
-            mBpfCoordinator.tetherOffloadRuleUpdate(this, upstreamIfindex);
+            mBpfCoordinator.updateAllIpv6Rules(this, this.mInterfaceParams,
+                    getInterfaceIndexForRule(upstreamIfindex, upstreamSupportsBpf));
         }
+    }
 
-        // If we're here to process a NeighborEvent, do so now.
+    // Handles updates to IPv6 downstream rules if a neighbor event is received.
+    private void addOrRemoveIpv6Downstream(NeighborEvent e) {
         // mInterfaceParams must be non-null or the event would not have arrived.
         if (e == null) return;
         if (!(e.ip instanceof Inet6Address) || e.ip.isMulticastAddress()
@@ -917,8 +921,9 @@
         // 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;
-        Ipv6DownstreamRule rule = new Ipv6DownstreamRule(upstreamIfindex, mInterfaceParams.index,
-                (Inet6Address) e.ip, mInterfaceParams.macAddr, dstMac);
+        Ipv6DownstreamRule rule = new Ipv6DownstreamRule(
+                getInterfaceIndexForRule(mLastIPv6UpstreamIfindex, mUpstreamSupportsBpf),
+                mInterfaceParams.index, (Inet6Address) e.ip, mInterfaceParams.macAddr, dstMac);
         if (e.isValid()) {
             mBpfCoordinator.addIpv6DownstreamRule(this, rule);
         } else {
@@ -951,8 +956,7 @@
         if (mInterfaceParams != null
                 && mInterfaceParams.index == e.ifindex
                 && mInterfaceParams.hasMacAddress) {
-            updateIpv6ForwardingRules(mLastIPv6UpstreamIfindex, mLastIPv6UpstreamIfindex,
-                    mUpstreamSupportsBpf, e);
+            addOrRemoveIpv6Downstream(e);
             updateClientInfoIpv4(e);
         }
     }
@@ -1285,6 +1289,7 @@
         @Override
         public void exit() {
             cleanupUpstream();
+            mBpfCoordinator.clearAllIpv6Rules(IpServer.this);
             super.exit();
         }
 
@@ -1303,7 +1308,8 @@
 
             for (String ifname : mUpstreamIfaceSet.ifnames) cleanupUpstreamInterface(ifname);
             mUpstreamIfaceSet = null;
-            mBpfCoordinator.tetherOffloadRuleClear(IpServer.this);
+            mBpfCoordinator.updateAllIpv6Rules(
+                    IpServer.this, IpServer.this.mInterfaceParams, NO_UPSTREAM);
         }
 
         private void cleanupUpstreamInterface(String upstreamIface) {
@@ -1370,7 +1376,7 @@
                             final InterfaceParams upstreamIfaceParams =
                                     mDeps.getInterfaceParams(ifname);
                             if (upstreamIfaceParams != null) {
-                                mBpfCoordinator.addUpstreamNameToLookupTable(
+                                mBpfCoordinator.maybeAddUpstreamToLookupTable(
                                         upstreamIfaceParams.index, ifname);
                             }
                         }
@@ -1430,6 +1436,8 @@
     class UnavailableState extends State {
         @Override
         public void enter() {
+            // TODO: move mIpNeighborMonitor.stop() to TetheredState#exit, and trigger a neighbours
+            //       dump after starting mIpNeighborMonitor.
             mIpNeighborMonitor.stop();
             mLastError = TETHER_ERROR_NO_ERROR;
             sendInterfaceState(STATE_UNAVAILABLE);
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 1b23a6c..46c815f 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -50,6 +50,7 @@
 import android.system.ErrnoException;
 import android.system.OsConstants;
 import android.text.TextUtils;
+import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.SparseArray;
@@ -153,6 +154,7 @@
     static final int NF_CONNTRACK_UDP_TIMEOUT_STREAM = 180;
     @VisibleForTesting
     static final int INVALID_MTU = 0;
+    static final int NO_UPSTREAM = 0;
 
     // List of TCP port numbers which aren't offloaded because the packets require the netfilter
     // conntrack helper. See also TetherController::setForwardRules in netd.
@@ -221,6 +223,23 @@
     // TODO: Remove the unused interface name.
     private final SparseArray<String> mInterfaceNames = new SparseArray<>();
 
+    // How IPv6 upstream rules and downstream rules are managed in BpfCoordinator:
+    // 1. Each upstream rule represents a downstream interface to an upstream interface forwarding.
+    //    No upstream rule will be exist if there is no upstream interface.
+    //    Note that there is at most one upstream interface for a given downstream interface.
+    // 2. Each downstream rule represents an IPv6 neighbor, regardless of the existence of the
+    //    upstream interface. If the upstream is not present, the downstream rules have an upstream
+    //    interface index of NO_UPSTREAM, only exist in BpfCoordinator and won't be written to the
+    //    BPF map. When the upstream comes back, those downstream rules will be updated by calling
+    //    Ipv6DownstreamRule#onNewUpstream and written to the BPF map again. We don't remove the
+    //    downstream rules when upstream is lost is because the upstream may come back with the
+    //    same prefix and we won't receive any neighbor update event in this case.
+    //    TODO: Remove downstream rules when upstream is lost and dump neighbors table when upstream
+    //    interface comes back in order to reconstruct the downstream rules.
+    // 3. It is the same thing for BpfCoordinator if there is no upstream interface or the upstream
+    //    interface is a virtual interface (which currently not supports BPF). In this case,
+    //    IpServer will update its upstream ifindex to NO_UPSTREAM to the BpfCoordinator.
+
     // Map of downstream rule maps. Each of these maps represents the IPv6 forwarding rules for a
     // given downstream. Each map:
     // - Is owned by the IpServer that is responsible for that downstream.
@@ -240,6 +259,16 @@
     private final HashMap<IpServer, LinkedHashMap<Inet6Address, Ipv6DownstreamRule>>
             mIpv6DownstreamRules = new LinkedHashMap<>();
 
+    // Map of IPv6 upstream rules maps. Each of these maps represents the IPv6 upstream rules for a
+    // given downstream. Each map:
+    // - Is owned by the IpServer that is responsible for that downstream.
+    // - Must only be modified by that IpServer.
+    // - Is created when the IpServer adds its first upstream rule, and deleted when the IpServer
+    //   deletes its last upstream rule (or clears its upstream rules)
+    // - Each upstream rule in the ArraySet is corresponding to an upstream interface.
+    private final ArrayMap<IpServer, ArraySet<Ipv6UpstreamRule>>
+            mIpv6UpstreamRules = new ArrayMap<>();
+
     // 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.
     // Each map:
@@ -603,130 +632,173 @@
     }
 
     /**
-     * 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.
+     * Add IPv6 upstream rule. After adding the first rule on a given upstream, must add the
+     * data limit on the given upstream.
      */
-    public void addIpv6DownstreamRule(
-            @NonNull final IpServer ipServer, @NonNull final Ipv6DownstreamRule rule) {
+    private void addIpv6UpstreamRule(
+            @NonNull final IpServer ipServer, @NonNull final Ipv6UpstreamRule rule) {
         if (!isUsingBpf()) return;
 
-        // TODO: Perhaps avoid to add a duplicate rule.
-        if (!mBpfCoordinatorShim.addIpv6DownstreamRule(rule)) return;
-
-        if (!mIpv6DownstreamRules.containsKey(ipServer)) {
-            mIpv6DownstreamRules.put(ipServer, new LinkedHashMap<Inet6Address,
-                    Ipv6DownstreamRule>());
-        }
-        LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules = mIpv6DownstreamRules.get(ipServer);
-
         // Add upstream and downstream interface index to dev map.
         maybeAddDevMap(rule.upstreamIfindex, rule.downstreamIfindex);
 
         // When the first rule is added to an upstream, setup upstream forwarding and data limit.
         maybeSetLimit(rule.upstreamIfindex);
 
-        if (!isAnyRuleFromDownstreamToUpstream(rule.downstreamIfindex, 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.
-            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);
-            }
+        // 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.addIpv6UpstreamRule(rule)) {
+            return;
         }
 
-        // Must update the adding rule after calling #isAnyRuleOnUpstream because it needs to
-        // check if it is about adding a first rule for a given upstream.
+        ArraySet<Ipv6UpstreamRule> rules = mIpv6UpstreamRules.computeIfAbsent(
+                ipServer, k -> new ArraySet<Ipv6UpstreamRule>());
+        rules.add(rule);
+    }
+
+    /**
+     * Clear all IPv6 upstream rules for a given downstream. 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.
+     */
+    private void clearIpv6UpstreamRules(@NonNull final IpServer ipServer) {
+        if (!isUsingBpf()) return;
+
+        final ArraySet<Ipv6UpstreamRule> upstreamRules = mIpv6UpstreamRules.remove(ipServer);
+        if (upstreamRules == null) return;
+
+        int upstreamIfindex = 0;
+        for (Ipv6UpstreamRule rule: upstreamRules) {
+            if (upstreamIfindex != 0 && rule.upstreamIfindex != upstreamIfindex) {
+                Log.wtf(TAG, "BUG: upstream rules point to more than one interface");
+            }
+            upstreamIfindex = rule.upstreamIfindex;
+            mBpfCoordinatorShim.removeIpv6UpstreamRule(rule);
+        }
+        // Clear the limit if there are no more rules on the given upstream.
+        // Using upstreamIfindex outside the loop is fine because all the rules for a given IpServer
+        // will always have the same upstream index (since they are always added all together by
+        // updateAllIpv6Rules).
+        // The upstreamIfindex can't be 0 because we won't add an Ipv6UpstreamRule with
+        // upstreamIfindex == 0 and if there is no Ipv6UpstreamRule for an IpServer, it will be
+        // removed from mIpv6UpstreamRules.
+        if (upstreamIfindex == 0) {
+            Log.wtf(TAG, "BUG: upstream rules have empty Set or rule.upstreamIfindex == 0");
+            return;
+        }
+        maybeClearLimit(upstreamIfindex);
+    }
+
+    /**
+     * Add IPv6 downstream rule.
+     * Note that this can be only called on handler thread.
+     */
+    public void addIpv6DownstreamRule(
+            @NonNull final IpServer ipServer, @NonNull final Ipv6DownstreamRule rule) {
+        if (!isUsingBpf()) return;
+
+        // TODO: Perhaps avoid to add a duplicate rule.
+        if (rule.upstreamIfindex != NO_UPSTREAM
+                && !mBpfCoordinatorShim.addIpv6DownstreamRule(rule)) return;
+
+        LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules =
+                mIpv6DownstreamRules.computeIfAbsent(ipServer,
+                        k -> new LinkedHashMap<Inet6Address, Ipv6DownstreamRule>());
         rules.put(rule.address, rule);
     }
 
     /**
-     * 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.
+     * Remove IPv6 downstream rule.
      * Note that this can be only called on handler thread.
      */
     public void removeIpv6DownstreamRule(
             @NonNull final IpServer ipServer, @NonNull final Ipv6DownstreamRule rule) {
         if (!isUsingBpf()) return;
 
-        if (!mBpfCoordinatorShim.removeIpv6DownstreamRule(rule)) return;
+        if (rule.upstreamIfindex != NO_UPSTREAM
+                && !mBpfCoordinatorShim.removeIpv6DownstreamRule(rule)) return;
 
         LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules = mIpv6DownstreamRules.get(ipServer);
         if (rules == null) return;
 
-        // Must remove rules before calling #isAnyRuleOnUpstream because it needs to check if
-        // the last rule is removed for a given upstream. If no rule is removed, return early.
-        // Avoid unnecessary work on a non-existent rule which may have never been added or
-        // removed already.
+        // If no rule is removed, return early. Avoid unnecessary work on a non-existent rule which
+        // may have never been added or removed already.
         if (rules.remove(rule.address) == null) return;
 
         // Remove the downstream entry if it has no more rule.
         if (rules.isEmpty()) {
             mIpv6DownstreamRules.remove(ipServer);
         }
+    }
 
-        // If no more rules between this upstream and downstream, stop upstream forwarding.
-        if (!isAnyRuleFromDownstreamToUpstream(rule.downstreamIfindex, rule.upstreamIfindex)) {
-            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);
-            }
+    /**
+      * Clear all downstream rules for a given IpServer and return a copy of all removed rules.
+      */
+    @Nullable
+    private Collection<Ipv6DownstreamRule> clearIpv6DownstreamRules(
+            @NonNull final IpServer ipServer) {
+        final LinkedHashMap<Inet6Address, Ipv6DownstreamRule> downstreamRules =
+                mIpv6DownstreamRules.remove(ipServer);
+        if (downstreamRules == null) return null;
+
+        final Collection<Ipv6DownstreamRule> removedRules = downstreamRules.values();
+        for (final Ipv6DownstreamRule rule : removedRules) {
+            if (rule.upstreamIfindex == NO_UPSTREAM) continue;
+            mBpfCoordinatorShim.removeIpv6DownstreamRule(rule);
         }
-
-        // Do cleanup functionality if there is no more rule on the given upstream.
-        maybeClearLimit(rule.upstreamIfindex);
+        return removedRules;
     }
 
     /**
      * Clear all forwarding rules for a given downstream.
      * Note that this can be only called on handler thread.
-     * TODO: rename to tetherOffloadRuleClear6 because of IPv6 only.
      */
-    public void tetherOffloadRuleClear(@NonNull final IpServer ipServer) {
+    public void clearAllIpv6Rules(@NonNull final IpServer ipServer) {
         if (!isUsingBpf()) return;
 
-        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 Ipv6DownstreamRule rule : new ArrayList<Ipv6DownstreamRule>(rules.values())) {
-            removeIpv6DownstreamRule(ipServer, rule);
-        }
+        // Clear downstream rules first, because clearing upstream rules fetches the stats, and
+        // fetching the stats requires that no rules be forwarding traffic to or from the upstream.
+        clearIpv6DownstreamRules(ipServer);
+        clearIpv6UpstreamRules(ipServer);
     }
 
     /**
-     * Update existing forwarding rules to new upstream for a given downstream.
+     * Delete all upstream and downstream rules for the passed-in IpServer, and if the new upstream
+     * is nonzero, reapply them to the new upstream.
      * Note that this can be only called on handler thread.
      */
-    public void tetherOffloadRuleUpdate(@NonNull final IpServer ipServer, int newUpstreamIfindex) {
+    public void updateAllIpv6Rules(@NonNull final IpServer ipServer,
+            final InterfaceParams interfaceParams, int newUpstreamIfindex) {
         if (!isUsingBpf()) return;
 
-        final LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules =
-                mIpv6DownstreamRules.get(ipServer);
-        if (rules == null) return;
+        // Remove IPv6 downstream rules. Remove the old ones before adding the new rules, otherwise
+        // we need to keep a copy of the old rules.
+        // We still need to keep the downstream rules even when the upstream goes away because it
+        // may come back with the same prefixes (unlikely, but possible). Neighbor entries won't be
+        // deleted and we're not expected to receive new Neighbor events in this case.
+        // TODO: Add new rule first to reduce the latency which has no rule. But this is okay
+        //       because if this is a new upstream, it will probably have different prefixes than
+        //       the one these downstream rules are in. If so, they will never see any downstream
+        //       traffic before new neighbor entries are created.
+        final Collection<Ipv6DownstreamRule> deletedDownstreamRules =
+                clearIpv6DownstreamRules(ipServer);
 
-        // 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 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<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.
-            removeIpv6DownstreamRule(ipServer, rule);
+        // Remove IPv6 upstream rules. Downstream rules must be removed first because
+        // BpfCoordinatorShimImpl#tetherOffloadGetAndClearStats will be called after the removal of
+        // the last upstream rule and it requires that no rules be forwarding traffic to or from
+        // that upstream.
+        clearIpv6UpstreamRules(ipServer);
+
+        // Add new upstream rules.
+        if (newUpstreamIfindex != 0 && interfaceParams != null && interfaceParams.macAddr != null) {
+            addIpv6UpstreamRule(ipServer, new Ipv6UpstreamRule(
+                    newUpstreamIfindex, interfaceParams.index, IPV6_ZERO_PREFIX64,
+                    interfaceParams.macAddr, NULL_MAC_ADDRESS, NULL_MAC_ADDRESS));
         }
-        for (final Ipv6DownstreamRule rule : rulesCopy) {
+
+        // Add updated downstream rules.
+        if (deletedDownstreamRules == null) return;
+        for (final Ipv6DownstreamRule rule : deletedDownstreamRules) {
             addIpv6DownstreamRule(ipServer, rule.onNewUpstream(newUpstreamIfindex));
         }
     }
@@ -737,7 +809,7 @@
      * expects the interface name in NetworkStats object.
      * Note that this can be only called on handler thread.
      */
-    public void addUpstreamNameToLookupTable(int upstreamIfindex, @NonNull String upstreamIface) {
+    public void maybeAddUpstreamToLookupTable(int upstreamIfindex, @Nullable String upstreamIface) {
         if (!isUsingBpf()) return;
 
         if (upstreamIfindex == 0 || TextUtils.isEmpty(upstreamIface)) return;
@@ -1007,7 +1079,7 @@
      * TODO: consider error handling if the attach program failed.
      */
     public void maybeAttachProgram(@NonNull String intIface, @NonNull String extIface) {
-        if (isVcnInterface(extIface)) return;
+        if (!isUsingBpf() || isVcnInterface(extIface)) return;
 
         if (forwardingPairExists(intIface, extIface)) return;
 
@@ -1031,6 +1103,8 @@
      * Detach BPF program
      */
     public void maybeDetachProgram(@NonNull String intIface, @NonNull String extIface) {
+        if (!isUsingBpf()) return;
+
         forwardingPairRemove(intIface, extIface);
 
         // Detaching program may fail because the interface has been removed already.
@@ -1967,9 +2041,8 @@
     }
 
     private int getInterfaceIndexFromRules(@NonNull String ifName) {
-        for (LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules :
-                mIpv6DownstreamRules.values()) {
-            for (Ipv6DownstreamRule rule : rules.values()) {
+        for (ArraySet<Ipv6UpstreamRule> rules : mIpv6UpstreamRules.values()) {
+            for (Ipv6UpstreamRule rule : rules) {
                 final int upstreamIfindex = rule.upstreamIfindex;
                 if (TextUtils.equals(ifName, mInterfaceNames.get(upstreamIfindex))) {
                     return upstreamIfindex;
@@ -1987,6 +2060,7 @@
     }
 
     private boolean sendDataLimitToBpfMap(int ifIndex, long quotaBytes) {
+        if (!isUsingBpf()) return false;
         if (ifIndex == 0) {
             Log.wtf(TAG, "Invalid interface index.");
             return false;
@@ -2060,28 +2134,14 @@
     // TODO: Rename to isAnyIpv6RuleOnUpstream and define an isAnyRuleOnUpstream method that called
     // both isAnyIpv6RuleOnUpstream and mBpfCoordinatorShim.isAnyIpv4RuleOnUpstream.
     private boolean isAnyRuleOnUpstream(int upstreamIfindex) {
-        for (LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules :
-                mIpv6DownstreamRules.values()) {
-            for (Ipv6DownstreamRule rule : rules.values()) {
+        for (ArraySet<Ipv6UpstreamRule> rules : mIpv6UpstreamRules.values()) {
+            for (Ipv6UpstreamRule rule : rules) {
                 if (upstreamIfindex == rule.upstreamIfindex) return true;
             }
         }
         return false;
     }
 
-    private boolean isAnyRuleFromDownstreamToUpstream(int downstreamIfindex, int upstreamIfindex) {
-        for (LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules :
-                mIpv6DownstreamRules.values()) {
-            for (Ipv6DownstreamRule rule : rules.values()) {
-                if (downstreamIfindex == rule.downstreamIfindex
-                        && upstreamIfindex == rule.upstreamIfindex) {
-                    return true;
-                }
-            }
-        }
-        return false;
-    }
-
     // TODO: remove the index from map while the interface has been removed because the map size
     // is 64 entries. See packages\modules\Connectivity\Tethering\bpf_progs\offload.c.
     private void maybeAddDevMap(int upstreamIfindex, int downstreamIfindex) {
diff --git a/Tethering/src/com/android/networkstack/tethering/util/SyncStateMachine.java b/Tethering/src/com/android/networkstack/tethering/util/SyncStateMachine.java
new file mode 100644
index 0000000..a17eb26
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/util/SyncStateMachine.java
@@ -0,0 +1,333 @@
+/**
+ * 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.networkstack.tethering.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Message;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.util.State;
+
+import java.util.ArrayDeque;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * An implementation of a state machine, meant to be called synchronously.
+ *
+ * This class implements a finite state automaton based on the same State
+ * class as StateMachine.
+ * All methods of this class must be called on only one thread.
+ */
+public class SyncStateMachine {
+    @NonNull private final String mName;
+    @NonNull private final Thread mMyThread;
+    private final boolean mDbg;
+    private final ArrayMap<State, StateInfo> mStateInfo = new ArrayMap<>();
+
+    // mCurrentState is the current state. mDestState is the target state that mCurrentState will
+    // transition to. The value of mDestState can be changed when a state processes a message and
+    // calls #transitionTo, but it cannot be changed during the state transition. When the state
+    // transition is complete, mDestState will be set to mCurrentState. Both mCurrentState and
+    // mDestState only be null before state machine starts and must only be touched on mMyThread.
+    @Nullable private State mCurrentState;
+    @Nullable private State mDestState;
+    private final ArrayDeque<Message> mSelfMsgQueue = new ArrayDeque<Message>();
+
+    // MIN_VALUE means not currently processing any message.
+    private int mCurrentlyProcessing = Integer.MIN_VALUE;
+    // Indicates whether automaton can send self message. Self messages can only be sent by
+    // automaton from State#enter, State#exit, or State#processMessage. Calling from outside
+    // of State is not allowed.
+    private boolean mSelfMsgAllowed = false;
+
+    /**
+     * A information class about a state and its parent. Used to maintain the state hierarchy.
+     */
+    public static class StateInfo {
+        /** The state who owns this StateInfo. */
+        public final State state;
+        /** The parent state. */
+        public final State parent;
+        // True when the state has been entered and on the stack.
+        private boolean mActive;
+
+        public StateInfo(@NonNull final State child, @Nullable final State parent) {
+            this.state = child;
+            this.parent = parent;
+        }
+    }
+
+    /**
+     * The constructor.
+     *
+     * @param name of this machine.
+     * @param thread the running thread of this machine. It must either be the thread on which this
+     * constructor is called, or a thread that is not started yet.
+     */
+    public SyncStateMachine(@NonNull final String name, @NonNull final Thread thread) {
+        this(name, thread, false /* debug */);
+    }
+
+    /**
+     * The constructor.
+     *
+     * @param name of this machine.
+     * @param thread the running thread of this machine. It must either be the thread on which this
+     * constructor is called, or a thread that is not started yet.
+     * @param dbg whether to print debug logs.
+     */
+    public SyncStateMachine(@NonNull final String name, @NonNull final Thread thread,
+            final boolean dbg) {
+        mMyThread = thread;
+        // Machine can either be setup from machine thread or before machine thread started.
+        ensureCorrectOrNotStartedThread();
+
+        mName = name;
+        mDbg = dbg;
+    }
+
+    /**
+     * Add all of states to the state machine. Different StateInfos which have same state are not
+     * allowed. In other words, a state can not have multiple parent states. #addAllStates can
+     * only be called once either from mMyThread or before mMyThread started.
+     */
+    public final void addAllStates(@NonNull final List<StateInfo> stateInfos) {
+        ensureCorrectOrNotStartedThread();
+
+        if (mCurrentState != null) {
+            throw new IllegalStateException("State only can be added before started");
+        }
+
+        if (stateInfos.isEmpty()) throw new IllegalStateException("Empty state is not allowed");
+
+        if (!mStateInfo.isEmpty()) throw new IllegalStateException("States are already configured");
+
+        final Set<Class> usedClasses = new ArraySet<>();
+        for (final StateInfo info : stateInfos) {
+            Objects.requireNonNull(info.state);
+            if (!usedClasses.add(info.state.getClass())) {
+                throw new IllegalStateException("Adding the same state multiple times in a state "
+                        + "machine is forbidden because it tends to be confusing; it can be done "
+                        + "with anonymous subclasses but consider carefully whether you want to "
+                        + "use a single state or other alternatives instead.");
+            }
+
+            mStateInfo.put(info.state, info);
+        }
+
+        // Check whether all of parent states indicated from StateInfo are added.
+        for (final StateInfo info : stateInfos) {
+            if (info.parent != null) ensureExistingState(info.parent);
+        }
+    }
+
+    /**
+     * Start the state machine. The initial state can't be child state.
+     *
+     * @param initialState the first state of this machine. The state must be exact state object
+     * setting up by {@link #addAllStates}, not a copy of it.
+     */
+    public final void start(@NonNull final State initialState) {
+        ensureCorrectThread();
+        ensureExistingState(initialState);
+
+        mDestState = initialState;
+        mSelfMsgAllowed = true;
+        performTransitions();
+        mSelfMsgAllowed = false;
+        // If sendSelfMessage was called inside initialState#enter(), mSelfMsgQueue must be
+        // processed.
+        maybeProcessSelfMessageQueue();
+    }
+
+    /**
+     * Process the message synchronously then perform state transition. This method is used
+     * externally to the automaton to request that the automaton process the given message.
+     * The message is processed sequentially, so calling this method recursively is not permitted.
+     * In other words, using this method inside State#enter, State#exit, or State#processMessage
+     * is incorrect and will result in an IllegalStateException.
+     */
+    public final void processMessage(int what, int arg1, int arg2, @Nullable Object obj) {
+        ensureCorrectThread();
+
+        if (mCurrentlyProcessing != Integer.MIN_VALUE) {
+            throw new IllegalStateException("Message(" + mCurrentlyProcessing
+                    + ") is still being processed");
+        }
+
+        // mCurrentlyProcessing tracks the external message request and it prevents this method to
+        // be called recursively. Once this message is processed and the transitions have been
+        // performed, the automaton will process the self message queue. The messages in the self
+        // message queue are added from within the automaton during processing external message.
+        // mCurrentlyProcessing is still the original external one and it will not prevent self
+        // messages from being processed.
+        mCurrentlyProcessing = what;
+        final Message msg = Message.obtain(null, what, arg1, arg2, obj);
+        currentStateProcessMessageThenPerformTransitions(msg);
+        msg.recycle();
+        maybeProcessSelfMessageQueue();
+
+        mCurrentlyProcessing = Integer.MIN_VALUE;
+    }
+
+    private void maybeProcessSelfMessageQueue() {
+        while (!mSelfMsgQueue.isEmpty()) {
+            currentStateProcessMessageThenPerformTransitions(mSelfMsgQueue.poll());
+        }
+    }
+
+    private void currentStateProcessMessageThenPerformTransitions(@NonNull final Message msg) {
+        mSelfMsgAllowed = true;
+        StateInfo consideredState = mStateInfo.get(mCurrentState);
+        while (null != consideredState) {
+            // Ideally this should compare with IState.HANDLED, but it is not public field so just
+            // checking whether the return value is true (IState.HANDLED = true).
+            if (consideredState.state.processMessage(msg)) {
+                if (mDbg) {
+                    Log.d(mName, "State " + consideredState.state
+                            + " processed message " + msg.what);
+                }
+                break;
+            }
+            consideredState = mStateInfo.get(consideredState.parent);
+        }
+        if (null == consideredState) {
+            Log.wtf(mName, "Message " + msg.what + " was not handled");
+        }
+
+        performTransitions();
+        mSelfMsgAllowed = false;
+    }
+
+    /**
+     * Send self message during state transition.
+     *
+     * Must only be used inside State processMessage, enter or exit. The typical use case is
+     * something wrong happens during state transition, sending an error message which would be
+     * handled after finishing current state transitions.
+     */
+    public final void sendSelfMessage(int what, int arg1, int arg2, Object obj) {
+        if (!mSelfMsgAllowed) {
+            throw new IllegalStateException("sendSelfMessage can only be called inside "
+                    + "State#enter, State#exit or State#processMessage");
+        }
+
+        mSelfMsgQueue.add(Message.obtain(null, what, arg1, arg2, obj));
+    }
+
+    /**
+     * Transition to destination state. Upon returning from processMessage the automaton will
+     * transition to the given destination state.
+     *
+     * This function can NOT be called inside the State enter and exit function. The transition
+     * target is always defined and can never be changed mid-way of state transition.
+     *
+     * @param destState will be the state to transition to. The state must be the same instance set
+     * up by {@link #addAllStates}, not a copy of it.
+     */
+    public final void transitionTo(@NonNull final State destState) {
+        if (mDbg) Log.d(mName, "transitionTo " + destState);
+        ensureCorrectThread();
+        ensureExistingState(destState);
+
+        if (mDestState == mCurrentState) {
+            mDestState = destState;
+        } else {
+            throw new IllegalStateException("Destination already specified");
+        }
+    }
+
+    private void performTransitions() {
+        // 1. Determine the common ancestor state of current/destination states
+        // 2. Invoke state exit list from current state to common ancestor state.
+        // 3. Invoke state enter list from common ancestor state to destState by going
+        // through mEnterStateStack.
+        if (mDestState == mCurrentState) return;
+
+        final StateInfo commonAncestor = getLastActiveAncestor(mStateInfo.get(mDestState));
+
+        executeExitMethods(commonAncestor, mStateInfo.get(mCurrentState));
+        executeEnterMethods(commonAncestor, mStateInfo.get(mDestState));
+        mCurrentState = mDestState;
+    }
+
+    // Null is the root of all states.
+    private StateInfo getLastActiveAncestor(@Nullable final StateInfo start) {
+        if (null == start || start.mActive) return start;
+
+        return getLastActiveAncestor(mStateInfo.get(start.parent));
+    }
+
+    // Call the exit method from current state to common ancestor state.
+    // Both the commonAncestor and exitingState StateInfo can be null because null is the ancestor
+    // of all states.
+    // For example: When transitioning from state1 to state2, the
+    // executeExitMethods(commonAncestor, exitingState) function will be called twice, once with
+    // null and state1 as the argument, and once with null and null as the argument.
+    //              root
+    //              |   \
+    // current <- state1 state2 -> destination
+    private void executeExitMethods(@Nullable StateInfo commonAncestor,
+            @Nullable StateInfo exitingState) {
+        if (commonAncestor == exitingState) return;
+
+        if (mDbg) Log.d(mName, exitingState.state + " exit()");
+        exitingState.state.exit();
+        exitingState.mActive = false;
+        executeExitMethods(commonAncestor, mStateInfo.get(exitingState.parent));
+    }
+
+    // Call the enter method from common ancestor state to destination state.
+    // Both the commonAncestor and enteringState StateInfo can be null because null is the ancestor
+    // of all states.
+    // For example: When transitioning from state1 to state2, the
+    // executeEnterMethods(commonAncestor, enteringState) function will be called twice, once with
+    // null and state2 as the argument, and once with null and null as the argument.
+    //              root
+    //              |   \
+    // current <- state1 state2 -> destination
+    private void executeEnterMethods(@Nullable StateInfo commonAncestor,
+            @Nullable StateInfo enteringState) {
+        if (enteringState == commonAncestor) return;
+
+        executeEnterMethods(commonAncestor, mStateInfo.get(enteringState.parent));
+        if (mDbg) Log.d(mName, enteringState.state + " enter()");
+        enteringState.state.enter();
+        enteringState.mActive = true;
+    }
+
+    private void ensureCorrectThread() {
+        if (!mMyThread.equals(Thread.currentThread())) {
+            throw new IllegalStateException("Called from wrong thread");
+        }
+    }
+
+    private void ensureCorrectOrNotStartedThread() {
+        if (!mMyThread.isAlive()) return;
+
+        ensureCorrectThread();
+    }
+
+    private void ensureExistingState(@NonNull final State state) {
+        if (!mStateInfo.containsKey(state)) throw new IllegalStateException("Invalid state");
+    }
+}
diff --git a/Tethering/tests/integration/Android.bp b/Tethering/tests/integration/Android.bp
index 20f0bc6..2594a5e 100644
--- a/Tethering/tests/integration/Android.bp
+++ b/Tethering/tests/integration/Android.bp
@@ -28,7 +28,7 @@
         "DhcpPacketLib",
         "androidx.test.rules",
         "cts-net-utils",
-        "mockito-target-extended-minus-junit4",
+        "mockito-target-minus-junit4",
         "net-tests-utils",
         "net-utils-device-common",
         "net-utils-device-common-bpf",
@@ -40,11 +40,6 @@
         "android.test.base",
         "android.test.mock",
     ],
-    jni_libs: [
-        // For mockito extended
-        "libdexmakerjvmtiagent",
-        "libstaticjvmtiagent",
-    ],
 }
 
 android_library {
@@ -54,6 +49,7 @@
     defaults: ["TetheringIntegrationTestsDefaults"],
     visibility: [
         "//packages/modules/Connectivity/Tethering/tests/mts",
+        "//packages/modules/Connectivity/tests/cts/net",
     ]
 }
 
diff --git a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
index 83fc3e4..0702aa7 100644
--- a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
+++ b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
@@ -31,14 +31,12 @@
 import static android.net.TetheringTester.isExpectedIcmpPacket;
 import static android.net.TetheringTester.isExpectedTcpPacket;
 import static android.net.TetheringTester.isExpectedUdpPacket;
-
 import static com.android.net.module.util.HexDump.dumpHexString;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
 import static com.android.net.module.util.NetworkStackConstants.TCPHDR_ACK;
 import static com.android.net.module.util.NetworkStackConstants.TCPHDR_SYN;
 import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
 import static com.android.testutils.TestPermissionUtil.runAsShell;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -164,6 +162,10 @@
     private TapPacketReader mDownstreamReader;
     private MyTetheringEventCallback mTetheringEventCallback;
 
+    public Context getContext() {
+        return mContext;
+    }
+
     @BeforeClass
     public static void setUpOnce() throws Exception {
         // The first test case may experience tethering restart with IP conflict handling.
diff --git a/Tethering/tests/integration/base/android/net/TetheringTester.java b/Tethering/tests/integration/base/android/net/TetheringTester.java
index 4f3c6e7..ae4ae55 100644
--- a/Tethering/tests/integration/base/android/net/TetheringTester.java
+++ b/Tethering/tests/integration/base/android/net/TetheringTester.java
@@ -27,12 +27,9 @@
 import static android.system.OsConstants.IPPROTO_IPV6;
 import static android.system.OsConstants.IPPROTO_TCP;
 import static android.system.OsConstants.IPPROTO_UDP;
-
 import static com.android.net.module.util.DnsPacket.ANSECTION;
-import static com.android.net.module.util.DnsPacket.ARSECTION;
 import static com.android.net.module.util.DnsPacket.DnsHeader;
 import static com.android.net.module.util.DnsPacket.DnsRecord;
-import static com.android.net.module.util.DnsPacket.NSSECTION;
 import static com.android.net.module.util.DnsPacket.QDSECTION;
 import static com.android.net.module.util.HexDump.dumpHexString;
 import static com.android.net.module.util.IpUtils.icmpChecksum;
@@ -56,7 +53,6 @@
 import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_OVERRIDE;
 import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED;
 import static com.android.net.module.util.NetworkStackConstants.TCPHDR_SYN;
-
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.fail;
 
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index eed308c..076fde3 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -326,6 +326,14 @@
 
             waitForRouterAdvertisement(downstreamReader, iface, WAIT_RA_TIMEOUT_MS);
             expectLocalOnlyAddresses(iface);
+
+            // After testing the IPv6 local address, the DHCP server may still be in the process
+            // of being created. If the downstream interface is killed by the test while the
+            // DHCP server is starting, a DHCP server error may occur. To ensure that the DHCP
+            // server has started completely before finishing the test, also test the dhcp server
+            // by calling runDhcp.
+            final TetheringTester tester = new TetheringTester(downstreamReader);
+            tester.runDhcp(MacAddress.fromString("1:2:3:4:5:6").toByteArray());
         } finally {
             maybeStopTapPacketReader(downstreamReader);
             maybeCloseTestInterface(downstreamIface);
diff --git a/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java b/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
index 81d4fbe..60f2d17 100644
--- a/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
+++ b/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
@@ -44,6 +44,7 @@
 import com.android.net.module.util.netlink.NetlinkUtils;
 import com.android.net.module.util.netlink.StructNlMsgHdr;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -84,6 +85,14 @@
         mOffloadHw = new OffloadHardwareInterface(mHandler, mLog, mDeps);
     }
 
+    @After
+    public void tearDown() throws Exception {
+        if (mHandlerThread != null) {
+            mHandlerThread.quitSafely();
+            mHandlerThread.join();
+        }
+    }
+
     void findConnectionOrThrow(FileDescriptor fd, InetSocketAddress local, InetSocketAddress remote)
             throws Exception {
         Log.d(TAG, "Looking for socket " + local + " -> " + remote);
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index c0718d1..d497a4d 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -158,6 +158,7 @@
     private static final String UPSTREAM_IFACE = "upstream0";
     private static final String UPSTREAM_IFACE2 = "upstream1";
     private static final String IPSEC_IFACE = "ipsec0";
+    private static final int NO_UPSTREAM = 0;
     private static final int UPSTREAM_IFINDEX = 101;
     private static final int UPSTREAM_IFINDEX2 = 102;
     private static final int IPSEC_IFINDEX = 103;
@@ -274,8 +275,18 @@
             LinkProperties lp = new LinkProperties();
             lp.setInterfaceName(upstreamIface);
             dispatchTetherConnectionChanged(upstreamIface, lp, 0);
+            if (usingBpfOffload) {
+                InterfaceParams interfaceParams = mDependencies.getInterfaceParams(upstreamIface);
+                assertNotNull("missing upstream interface: " + upstreamIface, interfaceParams);
+                verify(mBpfCoordinator).updateAllIpv6Rules(
+                        mIpServer, TEST_IFACE_PARAMS, interfaceParams.index);
+                verifyStartUpstreamIpv6Forwarding(null, interfaceParams.index);
+            } else {
+                verifyNoUpstreamIpv6ForwardingChange(null);
+            }
         }
-        reset(mNetd, mCallback, mAddressCoordinator, mBpfCoordinator);
+        reset(mCallback, mAddressCoordinator);
+        resetNetdBpfMapAndCoordinator();
         when(mAddressCoordinator.requestDownstreamAddress(any(), anyInt(),
                 anyBoolean())).thenReturn(mTestAddress);
     }
@@ -531,7 +542,7 @@
         InOrder inOrder = inOrder(mNetd, mBpfCoordinator);
 
         // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>.
-        inOrder.verify(mBpfCoordinator).addUpstreamNameToLookupTable(UPSTREAM_IFINDEX,
+        inOrder.verify(mBpfCoordinator).maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX,
                 UPSTREAM_IFACE);
         inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE);
         inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE);
@@ -553,7 +564,7 @@
         inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
 
         // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2>.
-        inOrder.verify(mBpfCoordinator).addUpstreamNameToLookupTable(UPSTREAM_IFINDEX2,
+        inOrder.verify(mBpfCoordinator).maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX2,
                 UPSTREAM_IFACE2);
         inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2);
         inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
@@ -578,7 +589,7 @@
 
         // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> and expect that failed on
         // tetherAddForward.
-        inOrder.verify(mBpfCoordinator).addUpstreamNameToLookupTable(UPSTREAM_IFINDEX2,
+        inOrder.verify(mBpfCoordinator).maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX2,
                 UPSTREAM_IFACE2);
         inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2);
         inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
@@ -606,7 +617,7 @@
 
         // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> and expect that failed on
         // ipfwdAddInterfaceForward.
-        inOrder.verify(mBpfCoordinator).addUpstreamNameToLookupTable(UPSTREAM_IFINDEX2,
+        inOrder.verify(mBpfCoordinator).maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX2,
                 UPSTREAM_IFACE2);
         inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2);
         inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
@@ -627,7 +638,15 @@
         inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE);
         inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
         inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer);
+        inOrder.verify(mBpfCoordinator).updateAllIpv6Rules(
+                mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM);
+        if (!mBpfDeps.isAtLeastS()) {
+            inOrder.verify(mNetd).tetherOffloadGetAndClearStats(UPSTREAM_IFINDEX);
+        }
+        // When tethering stops, upstream interface is set to zero and thus clearing all upstream
+        // rules. Downstream rules are needed to be cleared explicitly by calling
+        // BpfCoordinator#clearAllIpv6Rules in TetheredState#exit.
+        inOrder.verify(mBpfCoordinator).clearAllIpv6Rules(mIpServer);
         inOrder.verify(mNetd).tetherApplyDnsInterfaces();
         inOrder.verify(mNetd).tetherInterfaceRemove(IFACE_NAME);
         inOrder.verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
@@ -1064,13 +1083,12 @@
         recvNewNeigh(notMyIfindex, neighA, NUD_REACHABLE, macA);
         verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
 
-        // Events on this interface are received and sent to netd.
+        // Events on this interface are received and sent to BpfCoordinator.
         recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, 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);
@@ -1078,7 +1096,6 @@
                 mIpServer, makeDownstreamRule(UPSTREAM_IFINDEX, neighB, macB));
         verifyTetherOffloadRuleAdd(null,
                 UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
-        verifyNoUpstreamIpv6ForwardingChange(null);
         resetNetdBpfMapAndCoordinator();
 
         // Link-local and multicast neighbors are ignored.
@@ -1094,7 +1111,6 @@
                 mIpServer, makeDownstreamRule(UPSTREAM_IFINDEX, neighA, macNull));
         verifyTetherOffloadRuleRemove(null,
                 UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macNull);
-        verifyNoUpstreamIpv6ForwardingChange(null);
         resetNetdBpfMapAndCoordinator();
 
         // A neighbor that is deleted causes the rule to be removed.
@@ -1103,12 +1119,10 @@
                 mIpServer,  makeDownstreamRule(UPSTREAM_IFINDEX, neighB, macNull));
         verifyTetherOffloadRuleRemove(null,
                 UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macNull);
-        verifyStopUpstreamIpv6Forwarding(null);
         resetNetdBpfMapAndCoordinator();
 
         // Upstream changes result in updating the rules.
         recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA);
-        verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
         recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
         resetNetdBpfMapAndCoordinator();
 
@@ -1116,89 +1130,100 @@
         LinkProperties lp = new LinkProperties();
         lp.setInterfaceName(UPSTREAM_IFACE2);
         dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp, -1);
-        verify(mBpfCoordinator).tetherOffloadRuleUpdate(mIpServer, UPSTREAM_IFINDEX2);
+        verify(mBpfCoordinator).updateAllIpv6Rules(mIpServer, TEST_IFACE_PARAMS, UPSTREAM_IFINDEX2);
         verifyTetherOffloadRuleRemove(inOrder,
                 UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
         verifyTetherOffloadRuleRemove(inOrder,
                 UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
         verifyStopUpstreamIpv6Forwarding(inOrder);
-        verifyTetherOffloadRuleAdd(inOrder,
-                UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighA, macA);
         verifyStartUpstreamIpv6Forwarding(inOrder, UPSTREAM_IFINDEX2);
         verifyTetherOffloadRuleAdd(inOrder,
+                UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighA, macA);
+        verifyTetherOffloadRuleAdd(inOrder,
                 UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighB, macB);
-        verifyNoUpstreamIpv6ForwardingChange(inOrder);
         resetNetdBpfMapAndCoordinator();
 
         // When the upstream is lost, rules are removed.
         dispatchTetherConnectionChanged(null, null, 0);
-        // Clear function is called two times by:
+        // Upstream clear function is called two times by:
         // - processMessage CMD_TETHER_CONNECTION_CHANGED for the upstream is lost.
         // - processMessage CMD_IPV6_TETHER_UPDATE for the IPv6 upstream is lost.
         // See dispatchTetherConnectionChanged.
-        verify(mBpfCoordinator, times(2)).tetherOffloadRuleClear(mIpServer);
+        verify(mBpfCoordinator, times(2)).updateAllIpv6Rules(
+                mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM);
+        verifyStopUpstreamIpv6Forwarding(inOrder);
         verifyTetherOffloadRuleRemove(null,
                 UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighA, macA);
         verifyTetherOffloadRuleRemove(null,
                 UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighB, macB);
-        verifyStopUpstreamIpv6Forwarding(inOrder);
+        // Upstream lost doesn't clear the downstream rules from BpfCoordinator.
+        // Do that here.
+        recvDelNeigh(myIfindex, neighA, NUD_STALE, macA);
+        recvDelNeigh(myIfindex, neighB, NUD_STALE, macB);
+        verify(mBpfCoordinator).removeIpv6DownstreamRule(
+                mIpServer,  makeDownstreamRule(NO_UPSTREAM, neighA, macNull));
+        verify(mBpfCoordinator).removeIpv6DownstreamRule(
+                mIpServer,  makeDownstreamRule(NO_UPSTREAM, neighB, macNull));
         resetNetdBpfMapAndCoordinator();
 
-        // If the upstream is IPv4-only, no rules are added.
+        // If the upstream is IPv4-only, no IPv6 rules are added to BPF map.
         dispatchTetherConnectionChanged(UPSTREAM_IFACE);
         resetNetdBpfMapAndCoordinator();
         recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA);
-        // Clear function is called by #updateIpv6ForwardingRules for the IPv6 upstream is lost.
-        verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer);
         verifyNoUpstreamIpv6ForwardingChange(null);
+        // Downstream rules are only added to BpfCoordinator but not BPF map.
+        verify(mBpfCoordinator).addIpv6DownstreamRule(
+                mIpServer, makeDownstreamRule(NO_UPSTREAM, neighA, macA));
+        verifyNeverTetherOffloadRuleAdd();
         verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
 
-        // Rules can be added again once upstream IPv6 connectivity is available.
+        // Rules can be added again once upstream IPv6 connectivity is available. The existing rules
+        // with an upstream of NO_UPSTREAM are reapplied.
         lp.setInterfaceName(UPSTREAM_IFACE);
         dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, -1);
+        verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
+        verify(mBpfCoordinator).addIpv6DownstreamRule(
+                mIpServer, makeDownstreamRule(UPSTREAM_IFINDEX, neighA, macA));
+        verifyTetherOffloadRuleAdd(null,
+                UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
         recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, 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()).addIpv6DownstreamRule(
-                mIpServer, makeDownstreamRule(UPSTREAM_IFINDEX, neighA, macA));
-        verifyNeverTetherOffloadRuleAdd(
-                UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
 
         // If upstream IPv6 connectivity is lost, rules are removed.
         resetNetdBpfMapAndCoordinator();
         dispatchTetherConnectionChanged(UPSTREAM_IFACE, null, 0);
-        verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer);
+        verify(mBpfCoordinator).updateAllIpv6Rules(mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM);
         verifyTetherOffloadRuleRemove(null,
                 UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
         verifyStopUpstreamIpv6Forwarding(null);
 
-        // When the interface goes down, rules are removed.
+        // When upstream IPv6 connectivity comes back, upstream rules are added and downstream rules
+        // are reapplied.
         lp.setInterfaceName(UPSTREAM_IFACE);
         dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, -1);
-        recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA);
-        recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
+        verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
         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).addIpv6DownstreamRule(
                 mIpServer, makeDownstreamRule(UPSTREAM_IFINDEX, neighB, macB));
         verifyTetherOffloadRuleAdd(null,
                 UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
         resetNetdBpfMapAndCoordinator();
 
+        // When the downstream interface goes down, rules are removed.
         mIpServer.stop();
         mLooper.dispatchAll();
-        verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer);
+        verify(mBpfCoordinator).clearAllIpv6Rules(mIpServer);
+        verifyStopUpstreamIpv6Forwarding(null);
         verifyTetherOffloadRuleRemove(null,
                 UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
         verifyTetherOffloadRuleRemove(null,
                 UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
-        verifyStopUpstreamIpv6Forwarding(null);
         verify(mIpNeighborMonitor).stop();
         resetNetdBpfMapAndCoordinator();
     }
@@ -1228,7 +1253,6 @@
                 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);
@@ -1236,7 +1260,14 @@
                 mIpServer, makeDownstreamRule(UPSTREAM_IFINDEX, neigh, macNull));
         verifyTetherOffloadRuleRemove(null,
                 UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neigh, macNull);
-        verifyStopUpstreamIpv6Forwarding(null);
+        resetNetdBpfMapAndCoordinator();
+
+        // Upstream IPv6 connectivity change causes upstream rules change.
+        LinkProperties lp2 = new LinkProperties();
+        lp2.setInterfaceName(UPSTREAM_IFACE2);
+        dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp2, 0);
+        verify(mBpfCoordinator).updateAllIpv6Rules(mIpServer, TEST_IFACE_PARAMS, UPSTREAM_IFINDEX2);
+        verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX2);
         resetNetdBpfMapAndCoordinator();
 
         // [2] Disable BPF offload.
@@ -1247,12 +1278,16 @@
 
         recvNewNeigh(myIfindex, neigh, NUD_REACHABLE, macA);
         verifyNeverTetherOffloadRuleAdd();
-        verifyNoUpstreamIpv6ForwardingChange(null);
         resetNetdBpfMapAndCoordinator();
 
         recvDelNeigh(myIfindex, neigh, NUD_STALE, macA);
         verifyNeverTetherOffloadRuleRemove();
+        resetNetdBpfMapAndCoordinator();
+
+        // Upstream IPv6 connectivity change doesn't cause the rule to be added or removed.
+        dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp2, 0);
         verifyNoUpstreamIpv6ForwardingChange(null);
+        verifyNeverTetherOffloadRuleRemove();
         resetNetdBpfMapAndCoordinator();
     }
 
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 04eb430..601f587 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -169,6 +169,8 @@
     private static final String UPSTREAM_IFACE = "rmnet0";
     private static final String UPSTREAM_XLAT_IFACE = "v4-rmnet0";
     private static final String UPSTREAM_IFACE2 = "wlan0";
+    private static final String DOWNSTREAM_IFACE = "downstream1";
+    private static final String DOWNSTREAM_IFACE2 = "downstream2";
 
     private static final MacAddress DOWNSTREAM_MAC = MacAddress.fromString("12:34:56:78:90:ab");
     private static final MacAddress DOWNSTREAM_MAC2 = MacAddress.fromString("ab:90:78:56:34:12");
@@ -213,6 +215,11 @@
     private static final InterfaceParams UPSTREAM_IFACE_PARAMS2 = new InterfaceParams(
             UPSTREAM_IFACE2, UPSTREAM_IFINDEX2, MacAddress.fromString("44:55:66:00:00:0c"),
             NetworkStackConstants.ETHER_MTU);
+    private static final InterfaceParams DOWNSTREAM_IFACE_PARAMS = new InterfaceParams(
+            DOWNSTREAM_IFACE, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC, NetworkStackConstants.ETHER_MTU);
+    private static final InterfaceParams DOWNSTREAM_IFACE_PARAMS2 = new InterfaceParams(
+            DOWNSTREAM_IFACE2, DOWNSTREAM_IFINDEX2, DOWNSTREAM_MAC2,
+            NetworkStackConstants.ETHER_MTU);
 
     private static final Map<Integer, UpstreamInformation> UPSTREAM_INFORMATIONS = Map.of(
             UPSTREAM_IFINDEX, new UpstreamInformation(UPSTREAM_IFACE_PARAMS,
@@ -640,24 +647,6 @@
         }
     }
 
-    private void verifyStartUpstreamIpv6Forwarding(@Nullable InOrder inOrder, int downstreamIfIndex,
-            MacAddress downstreamMac, int upstreamIfindex) throws Exception {
-        if (!mDeps.isAtLeastS()) return;
-        final TetherUpstream6Key key = new TetherUpstream6Key(downstreamIfIndex, downstreamMac, 0);
-        final Tether6Value value = new Tether6Value(upstreamIfindex,
-                MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS,
-                ETH_P_IPV6, NetworkStackConstants.ETHER_MTU);
-        verifyWithOrder(inOrder, mBpfUpstream6Map).insertEntry(key, value);
-    }
-
-    private void verifyStopUpstreamIpv6Forwarding(@Nullable InOrder inOrder, int downstreamIfIndex,
-            MacAddress downstreamMac)
-            throws Exception {
-        if (!mDeps.isAtLeastS()) return;
-        final TetherUpstream6Key key = new TetherUpstream6Key(downstreamIfIndex, downstreamMac, 0);
-        verifyWithOrder(inOrder, mBpfUpstream6Map).deleteEntry(key);
-    }
-
     private void verifyNoUpstreamIpv6ForwardingChange(@Nullable InOrder inOrder) throws Exception {
         if (!mDeps.isAtLeastS()) return;
         if (inOrder != null) {
@@ -671,6 +660,13 @@
         }
     }
 
+    private void verifyAddUpstreamRule(@Nullable InOrder inOrder,
+            @NonNull Ipv6UpstreamRule rule) throws Exception {
+        if (!mDeps.isAtLeastS()) return;
+        verifyWithOrder(inOrder, mBpfUpstream6Map).insertEntry(
+                rule.makeTetherUpstream6Key(), rule.makeTether6Value());
+    }
+
     private void verifyAddDownstreamRule(@Nullable InOrder inOrder,
             @NonNull Ipv6DownstreamRule rule) throws Exception {
         if (mDeps.isAtLeastS()) {
@@ -681,6 +677,11 @@
         }
     }
 
+    private void verifyNeverAddUpstreamRule() throws Exception {
+        if (!mDeps.isAtLeastS()) return;
+        verify(mBpfUpstream6Map, never()).insertEntry(any(), any());
+    }
+
     private void verifyNeverAddDownstreamRule() throws Exception {
         if (mDeps.isAtLeastS()) {
             verify(mBpfDownstream6Map, never()).updateEntry(any(), any());
@@ -689,6 +690,13 @@
         }
     }
 
+    private void verifyRemoveUpstreamRule(@Nullable InOrder inOrder,
+            @NonNull final Ipv6UpstreamRule rule) throws Exception {
+        if (!mDeps.isAtLeastS()) return;
+        verifyWithOrder(inOrder, mBpfUpstream6Map).deleteEntry(
+                rule.makeTetherUpstream6Key());
+    }
+
     private void verifyRemoveDownstreamRule(@Nullable InOrder inOrder,
             @NonNull final Ipv6DownstreamRule rule) throws Exception {
         if (mDeps.isAtLeastS()) {
@@ -699,6 +707,11 @@
         }
     }
 
+    private void verifyNeverRemoveUpstreamRule() throws Exception {
+        if (!mDeps.isAtLeastS()) return;
+        verify(mBpfUpstream6Map, never()).deleteEntry(any());
+    }
+
     private void verifyNeverRemoveDownstreamRule() throws Exception {
         if (mDeps.isAtLeastS()) {
             verify(mBpfDownstream6Map, never()).deleteEntry(any());
@@ -763,24 +776,31 @@
 
         final String mobileIface = "rmnet_data0";
         final Integer mobileIfIndex = 100;
-        coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
+        coordinator.maybeAddUpstreamToLookupTable(mobileIfIndex, mobileIface);
 
         // InOrder is required because mBpfStatsMap may be accessed by both
         // BpfCoordinator#tetherOffloadRuleAdd and BpfCoordinator#tetherOffloadGetAndClearStats.
         // 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 Ipv6DownstreamRule rule = buildTestDownstreamRule(mobileIfIndex, NEIGH_A, MAC_A);
-        coordinator.addIpv6DownstreamRule(mIpServer, rule);
-        verifyAddDownstreamRule(inOrder, rule);
+        final InOrder inOrder = inOrder(mNetd, mBpfUpstream6Map, mBpfDownstream6Map, mBpfLimitMap,
+                mBpfStatsMap);
+        final Ipv6UpstreamRule upstreamRule = buildTestUpstreamRule(
+                mobileIfIndex, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC);
+        final Ipv6DownstreamRule downstreamRule = buildTestDownstreamRule(
+                mobileIfIndex, NEIGH_A, MAC_A);
+        coordinator.updateAllIpv6Rules(mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex);
         verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
                 true /* isInit */);
+        verifyAddUpstreamRule(inOrder, upstreamRule);
+        coordinator.addIpv6DownstreamRule(mIpServer, downstreamRule);
+        verifyAddDownstreamRule(inOrder, downstreamRule);
 
-        // Removing the last rule on current upstream immediately sends the cleanup stuff to netd.
+        // Removing the last rule on current upstream immediately sends the cleanup stuff to BPF.
         updateStatsEntryForTetherOffloadGetAndClearStats(
                 buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0));
-        coordinator.removeIpv6DownstreamRule(mIpServer, rule);
-        verifyRemoveDownstreamRule(inOrder, rule);
+        coordinator.updateAllIpv6Rules(mIpServer, DOWNSTREAM_IFACE_PARAMS, 0);
+        verifyRemoveDownstreamRule(inOrder, downstreamRule);
+        verifyRemoveUpstreamRule(inOrder, upstreamRule);
         verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
     }
 
@@ -806,7 +826,7 @@
 
         final String mobileIface = "rmnet_data0";
         final Integer mobileIfIndex = 100;
-        coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
+        coordinator.maybeAddUpstreamToLookupTable(mobileIfIndex, mobileIface);
 
         updateStatsEntriesAndWaitForUpdate(new TetherStatsParcel[] {
                 buildTestTetherStatsParcel(mobileIfIndex, 1000, 100, 2000, 200)});
@@ -847,8 +867,8 @@
 
         // Add interface name to lookup table. In realistic case, the upstream interface name will
         // be added by IpServer when IpServer has received with a new IPv6 upstream update event.
-        coordinator.addUpstreamNameToLookupTable(wlanIfIndex, wlanIface);
-        coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
+        coordinator.maybeAddUpstreamToLookupTable(wlanIfIndex, wlanIface);
+        coordinator.maybeAddUpstreamToLookupTable(mobileIfIndex, mobileIface);
 
         // [1] Both interface stats are changed.
         // Setup the tether stats of wlan and mobile interface. Note that move forward the time of
@@ -912,7 +932,7 @@
 
         final String mobileIface = "rmnet_data0";
         final Integer mobileIfIndex = 100;
-        coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
+        coordinator.maybeAddUpstreamToLookupTable(mobileIfIndex, mobileIface);
 
         // Verify that set quota to 0 will immediately triggers a callback.
         mTetherStatsProvider.onSetAlert(0);
@@ -978,9 +998,10 @@
     }
 
     @NonNull
-    private static Ipv6UpstreamRule buildTestUpstreamRule(int upstreamIfindex) {
-        return new Ipv6UpstreamRule(upstreamIfindex, DOWNSTREAM_IFINDEX,
-                IPV6_ZERO_PREFIX, DOWNSTREAM_MAC, MacAddress.ALL_ZEROS_ADDRESS,
+    private static Ipv6UpstreamRule buildTestUpstreamRule(
+            int upstreamIfindex, int downstreamIfindex, @NonNull MacAddress inDstMac) {
+        return new Ipv6UpstreamRule(upstreamIfindex, downstreamIfindex,
+                IPV6_ZERO_PREFIX, inDstMac, MacAddress.ALL_ZEROS_ADDRESS,
                 MacAddress.ALL_ZEROS_ADDRESS);
     }
 
@@ -1027,17 +1048,18 @@
 
         final String mobileIface = "rmnet_data0";
         final int mobileIfIndex = 100;
-        coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
+        coordinator.maybeAddUpstreamToLookupTable(mobileIfIndex, mobileIface);
 
         // [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 Ipv6DownstreamRule rule = buildTestDownstreamRule(mobileIfIndex, NEIGH_A, MAC_A);
-        final InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfLimitMap, mBpfStatsMap);
-        coordinator.addIpv6DownstreamRule(mIpServer, rule);
-        verifyAddDownstreamRule(inOrder, rule);
+        final Ipv6UpstreamRule rule = buildTestUpstreamRule(
+                mobileIfIndex, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC);
+        final InOrder inOrder = inOrder(mNetd, mBpfUpstream6Map, mBpfLimitMap, mBpfStatsMap);
+        coordinator.updateAllIpv6Rules(mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex);
         verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
                 true /* isInit */);
+        verifyAddUpstreamRule(inOrder, rule);
         inOrder.verifyNoMoreInteractions();
 
         // [2] Specific limit.
@@ -1062,7 +1084,6 @@
         }
     }
 
-    // TODO: Test the case in which the rules are changed from different IpServer objects.
     @Test
     public void testSetDataLimitOnRule6Change() throws Exception {
         setupFunctioningNetdInterface();
@@ -1071,39 +1092,41 @@
 
         final String mobileIface = "rmnet_data0";
         final int mobileIfIndex = 100;
-        coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
+        coordinator.maybeAddUpstreamToLookupTable(mobileIfIndex, mobileIface);
 
         // Applying a data limit to the current upstream does not take any immediate action.
         // The data limit could be only set on an upstream which has rules.
         final long limit = 12345;
-        final InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfLimitMap, mBpfStatsMap);
+        final InOrder inOrder = inOrder(mNetd, mBpfUpstream6Map, mBpfLimitMap, mBpfStatsMap);
         mTetherStatsProvider.onSetLimit(mobileIface, limit);
         waitForIdle();
         verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
 
-        // Adding the first rule on current upstream immediately sends the quota to netd.
-        final Ipv6DownstreamRule ruleA = buildTestDownstreamRule(mobileIfIndex, NEIGH_A, MAC_A);
-        coordinator.addIpv6DownstreamRule(mIpServer, ruleA);
-        verifyAddDownstreamRule(inOrder, ruleA);
+        // Adding the first rule on current upstream immediately sends the quota to BPF.
+        final Ipv6UpstreamRule ruleA = buildTestUpstreamRule(
+                mobileIfIndex, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC);
+        coordinator.updateAllIpv6Rules(mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex);
         verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, limit, true /* isInit */);
+        verifyAddUpstreamRule(inOrder, ruleA);
         inOrder.verifyNoMoreInteractions();
 
-        // Adding the second rule on current upstream does not send the quota to netd.
-        final Ipv6DownstreamRule ruleB = buildTestDownstreamRule(mobileIfIndex, NEIGH_B, MAC_B);
-        coordinator.addIpv6DownstreamRule(mIpServer, ruleB);
-        verifyAddDownstreamRule(inOrder, ruleB);
+        // Adding the second rule on current upstream does not send the quota to BPF.
+        final Ipv6UpstreamRule ruleB = buildTestUpstreamRule(
+                mobileIfIndex, DOWNSTREAM_IFINDEX2, DOWNSTREAM_MAC2);
+        coordinator.updateAllIpv6Rules(mIpServer2, DOWNSTREAM_IFACE_PARAMS2, mobileIfIndex);
+        verifyAddUpstreamRule(inOrder, ruleB);
         verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
 
-        // Removing the second rule on current upstream does not send the quota to netd.
-        coordinator.removeIpv6DownstreamRule(mIpServer, ruleB);
-        verifyRemoveDownstreamRule(inOrder, ruleB);
+        // Removing the second rule on current upstream does not send the quota to BPF.
+        coordinator.updateAllIpv6Rules(mIpServer2, DOWNSTREAM_IFACE_PARAMS2, 0);
+        verifyRemoveUpstreamRule(inOrder, ruleB);
         verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
 
-        // Removing the last rule on current upstream immediately sends the cleanup stuff to netd.
+        // Removing the last rule on current upstream immediately sends the cleanup stuff to BPF.
         updateStatsEntryForTetherOffloadGetAndClearStats(
                 buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0));
-        coordinator.removeIpv6DownstreamRule(mIpServer, ruleA);
-        verifyRemoveDownstreamRule(inOrder, ruleA);
+        coordinator.updateAllIpv6Rules(mIpServer, DOWNSTREAM_IFACE_PARAMS, 0);
+        verifyRemoveUpstreamRule(inOrder, ruleA);
         verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
         inOrder.verifyNoMoreInteractions();
     }
@@ -1118,8 +1141,8 @@
         final String mobileIface = "rmnet_data0";
         final Integer ethIfIndex = 100;
         final Integer mobileIfIndex = 101;
-        coordinator.addUpstreamNameToLookupTable(ethIfIndex, ethIface);
-        coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
+        coordinator.maybeAddUpstreamToLookupTable(ethIfIndex, ethIface);
+        coordinator.maybeAddUpstreamToLookupTable(mobileIfIndex, mobileIface);
 
         final InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfUpstream6Map, mBpfLimitMap,
                 mBpfStatsMap);
@@ -1133,20 +1156,25 @@
 
         // [1] Adding rules on the upstream Ethernet.
         // Note that the default data limit is applied after the first rule is added.
+        final Ipv6UpstreamRule ethernetUpstreamRule = buildTestUpstreamRule(
+                ethIfIndex, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC);
         final Ipv6DownstreamRule ethernetRuleA = buildTestDownstreamRule(
                 ethIfIndex, NEIGH_A, MAC_A);
         final Ipv6DownstreamRule ethernetRuleB = buildTestDownstreamRule(
                 ethIfIndex, NEIGH_B, MAC_B);
 
-        coordinator.addIpv6DownstreamRule(mIpServer, ethernetRuleA);
-        verifyAddDownstreamRule(inOrder, ethernetRuleA);
+        coordinator.updateAllIpv6Rules(mIpServer, DOWNSTREAM_IFACE_PARAMS, ethIfIndex);
         verifyTetherOffloadSetInterfaceQuota(inOrder, ethIfIndex, QUOTA_UNLIMITED,
                 true /* isInit */);
-        verifyStartUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC, ethIfIndex);
+        verifyAddUpstreamRule(inOrder, ethernetUpstreamRule);
+        coordinator.addIpv6DownstreamRule(mIpServer, ethernetRuleA);
+        verifyAddDownstreamRule(inOrder, ethernetRuleA);
         coordinator.addIpv6DownstreamRule(mIpServer, ethernetRuleB);
         verifyAddDownstreamRule(inOrder, ethernetRuleB);
 
         // [2] Update the existing rules from Ethernet to cellular.
+        final Ipv6UpstreamRule mobileUpstreamRule = buildTestUpstreamRule(
+                mobileIfIndex, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC);
         final Ipv6DownstreamRule mobileRuleA = buildTestDownstreamRule(
                 mobileIfIndex, NEIGH_A, MAC_A);
         final Ipv6DownstreamRule mobileRuleB = buildTestDownstreamRule(
@@ -1156,25 +1184,24 @@
 
         // 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);
+        coordinator.updateAllIpv6Rules(mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex);
         verifyRemoveDownstreamRule(inOrder, ethernetRuleA);
         verifyRemoveDownstreamRule(inOrder, ethernetRuleB);
-        verifyStopUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC);
+        verifyRemoveUpstreamRule(inOrder, ethernetUpstreamRule);
         verifyTetherOffloadGetAndClearStats(inOrder, ethIfIndex);
-        verifyAddDownstreamRule(inOrder, mobileRuleA);
         verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
                 true /* isInit */);
-        verifyStartUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC,
-                mobileIfIndex);
+        verifyAddUpstreamRule(inOrder, mobileUpstreamRule);
+        verifyAddDownstreamRule(inOrder, mobileRuleA);
         verifyAddDownstreamRule(inOrder, mobileRuleB);
 
         // [3] Clear all rules for a given IpServer.
         updateStatsEntryForTetherOffloadGetAndClearStats(
                 buildTestTetherStatsParcel(mobileIfIndex, 50, 60, 70, 80));
-        coordinator.tetherOffloadRuleClear(mIpServer);
+        coordinator.clearAllIpv6Rules(mIpServer);
         verifyRemoveDownstreamRule(inOrder, mobileRuleA);
         verifyRemoveDownstreamRule(inOrder, mobileRuleB);
-        verifyStopUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC);
+        verifyRemoveUpstreamRule(inOrder, mobileUpstreamRule);
         verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
 
         // [4] Force pushing stats update to verify that the last diff of stats is reported on all
@@ -1204,7 +1231,7 @@
         // The interface name lookup table can't be added.
         final String iface = "rmnet_data0";
         final Integer ifIndex = 100;
-        coordinator.addUpstreamNameToLookupTable(ifIndex, iface);
+        coordinator.maybeAddUpstreamToLookupTable(ifIndex, iface);
         assertEquals(0, coordinator.getInterfaceNamesForTesting().size());
 
         // The rule can't be added.
@@ -1230,14 +1257,15 @@
         assertEquals(1, rules.size());
 
         // The rule can't be cleared.
-        coordinator.tetherOffloadRuleClear(mIpServer);
+        coordinator.clearAllIpv6Rules(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 */);
+        coordinator.updateAllIpv6Rules(
+                mIpServer, DOWNSTREAM_IFACE_PARAMS, rule.upstreamIfindex + 1 /* new */);
         verifyNeverRemoveDownstreamRule();
         verifyNeverAddDownstreamRule();
         rules = coordinator.getIpv6DownstreamRulesForTesting().get(mIpServer);
@@ -1552,7 +1580,7 @@
         // interface index.
         doReturn(upstreamInfo.interfaceParams).when(mDeps).getInterfaceParams(
                 upstreamInfo.interfaceParams.name);
-        coordinator.addUpstreamNameToLookupTable(upstreamInfo.interfaceParams.index,
+        coordinator.maybeAddUpstreamToLookupTable(upstreamInfo.interfaceParams.index,
                 upstreamInfo.interfaceParams.name);
 
         final LinkProperties lp = new LinkProperties();
@@ -1677,19 +1705,21 @@
     public void testAddDevMapRule6() throws Exception {
         final BpfCoordinator coordinator = makeBpfCoordinator();
 
-        coordinator.addUpstreamNameToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE);
-        final Ipv6DownstreamRule ruleA = buildTestDownstreamRule(UPSTREAM_IFINDEX, NEIGH_A, MAC_A);
-        final Ipv6DownstreamRule ruleB = buildTestDownstreamRule(UPSTREAM_IFINDEX, NEIGH_B, MAC_B);
-
-        coordinator.addIpv6DownstreamRule(mIpServer, ruleA);
+        coordinator.maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE);
+        coordinator.updateAllIpv6Rules(mIpServer, DOWNSTREAM_IFACE_PARAMS, UPSTREAM_IFINDEX);
         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.addIpv6DownstreamRule(mIpServer, ruleB);
-        verify(mBpfDevMap, never()).updateEntry(any(), any());
+        // Adding the second downstream, only the second downstream ifindex is added to DevMap,
+        // the existing upstream ifindex won't be added again.
+        coordinator.updateAllIpv6Rules(mIpServer2, DOWNSTREAM_IFACE_PARAMS2, UPSTREAM_IFINDEX);
+        verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(DOWNSTREAM_IFINDEX2)),
+                eq(new TetherDevValue(DOWNSTREAM_IFINDEX2)));
+        verify(mBpfDevMap, never()).updateEntry(eq(new TetherDevKey(UPSTREAM_IFINDEX)),
+                eq(new TetherDevValue(UPSTREAM_IFINDEX)));
     }
 
     @Test
@@ -2153,7 +2183,8 @@
         assertEquals("upstreamIfindex: 1001, downstreamIfindex: 2001, address: 2001:db8::1, "
                 + "srcMac: 12:34:56:78:90:ab, dstMac: 00:00:00:00:00:0a",
                 downstreamRule.toString());
-        final Ipv6UpstreamRule upstreamRule = buildTestUpstreamRule(UPSTREAM_IFINDEX);
+        final Ipv6UpstreamRule upstreamRule = buildTestUpstreamRule(
+                UPSTREAM_IFINDEX, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC);
         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());
@@ -2221,7 +2252,7 @@
                         0L /* txPackets */, 0L /* txBytes */, 0L /* txErrors */));
 
         // dumpDevmap
-        coordinator.addUpstreamNameToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE);
+        coordinator.maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE);
         mBpfDevMap.insertEntry(
                 new TetherDevKey(UPSTREAM_IFINDEX),
                 new TetherDevValue(UPSTREAM_IFINDEX));
@@ -2390,7 +2421,7 @@
         // +-------+-------+-------+-------+-------+
 
         // [1] Mobile IPv4 only
-        coordinator.addUpstreamNameToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE);
+        coordinator.maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE);
         doReturn(UPSTREAM_IFACE_PARAMS).when(mDeps).getInterfaceParams(UPSTREAM_IFACE);
         final UpstreamNetworkState mobileIPv4UpstreamState = new UpstreamNetworkState(
                 buildUpstreamLinkProperties(UPSTREAM_IFACE,
@@ -2442,7 +2473,7 @@
         verifyIpv4Upstream(ipv4UpstreamIndices, interfaceNames);
 
         // Mobile IPv6 and xlat
-        // IpServer doesn't add xlat interface mapping via #addUpstreamNameToLookupTable on
+        // IpServer doesn't add xlat interface mapping via #maybeAddUpstreamToLookupTable on
         // S and T devices.
         coordinator.updateUpstreamNetworkState(mobile464xlatUpstreamState);
         // Upstream IPv4 address mapping is removed because xlat interface is not supported.
@@ -2457,7 +2488,7 @@
 
         // [6] Wifi IPv4 and IPv6
         // Expect that upstream index map is cleared because ether ip is not supported.
-        coordinator.addUpstreamNameToLookupTable(UPSTREAM_IFINDEX2, UPSTREAM_IFACE2);
+        coordinator.maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX2, UPSTREAM_IFACE2);
         doReturn(UPSTREAM_IFACE_PARAMS2).when(mDeps).getInterfaceParams(UPSTREAM_IFACE2);
         final UpstreamNetworkState wifiDualStackUpstreamState = new UpstreamNetworkState(
                 buildUpstreamLinkProperties(UPSTREAM_IFACE2,
diff --git a/bpf_progs/block.c b/bpf_progs/block.c
index d734b74..0a2b0b8 100644
--- a/bpf_progs/block.c
+++ b/bpf_progs/block.c
@@ -24,8 +24,8 @@
 
 #include "bpf_helpers.h"
 
-#define ALLOW 1
-#define DISALLOW 0
+static const int ALLOW = 1;
+static const int DISALLOW = 0;
 
 DEFINE_BPF_MAP_GRW(blocked_ports_map, ARRAY, int, uint64_t,
         1024 /* 64K ports -> 1024 u64s */, AID_SYSTEM)
@@ -57,14 +57,18 @@
     return ALLOW;
 }
 
-DEFINE_BPF_PROG_KVER("bind4/block_port", AID_ROOT, AID_SYSTEM,
-                     bind4_block_port, KVER(5, 4, 0))
+// the program need to be accessible/loadable by netd (from netd updatable plugin)
+#define DEFINE_NETD_RO_BPF_PROG(SECTION_NAME, the_prog, min_kver) \
+    DEFINE_BPF_PROG_EXT(SECTION_NAME, AID_ROOT, AID_ROOT, the_prog, min_kver, KVER_INF,  \
+                        BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, MANDATORY, \
+                        "", "netd_readonly/", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+
+DEFINE_NETD_RO_BPF_PROG("bind4/block_port", bind4_block_port, KVER_4_19)
 (struct bpf_sock_addr *ctx) {
     return block_port(ctx);
 }
 
-DEFINE_BPF_PROG_KVER("bind6/block_port", AID_ROOT, AID_SYSTEM,
-                     bind6_block_port, KVER(5, 4, 0))
+DEFINE_NETD_RO_BPF_PROG("bind6/block_port", bind6_block_port, KVER_4_19)
 (struct bpf_sock_addr *ctx) {
     return block_port(ctx);
 }
diff --git a/bpf_progs/bpf_net_helpers.h b/bpf_progs/bpf_net_helpers.h
index ed33cc9..f3c7de5 100644
--- a/bpf_progs/bpf_net_helpers.h
+++ b/bpf_progs/bpf_net_helpers.h
@@ -87,29 +87,18 @@
     if (skb->data_end - skb->data < len) bpf_skb_pull_data(skb, len);
 }
 
-// constants for passing in to 'bool egress'
-static const bool INGRESS = false;
-static const bool EGRESS = true;
+struct egress_bool { bool egress; };
+#define INGRESS ((struct egress_bool){ .egress = false })
+#define EGRESS ((struct egress_bool){ .egress = true })
 
-// constants for passing in to 'bool downstream'
-static const bool UPSTREAM = false;
-static const bool DOWNSTREAM = true;
+struct stream_bool { bool down; };
+#define UPSTREAM ((struct stream_bool){ .down = false })
+#define DOWNSTREAM ((struct stream_bool){ .down = true })
 
-// constants for passing in to 'bool is_ethernet'
-static const bool RAWIP = false;
-static const bool ETHER = true;
+struct rawip_bool { bool rawip; };
+#define ETHER ((struct rawip_bool){ .rawip = false })
+#define RAWIP ((struct rawip_bool){ .rawip = true })
 
-// constants for passing in to 'bool updatetime'
-static const bool NO_UPDATETIME = false;
-static const bool UPDATETIME = true;
-
-// constants for passing in to ignore_on_eng / ignore_on_user / ignore_on_userdebug
-// define's instead of static const due to tm-mainline-prod compiler static_assert limitations
-#define LOAD_ON_ENG false
-#define LOAD_ON_USER false
-#define LOAD_ON_USERDEBUG false
-#define IGNORE_ON_ENG true
-#define IGNORE_ON_USER true
-#define IGNORE_ON_USERDEBUG true
-
-#define KVER_4_14 KVER(4, 14, 0)
+struct updatetime_bool { bool updatetime; };
+#define NO_UPDATETIME ((struct updatetime_bool){ .updatetime = false })
+#define UPDATETIME ((struct updatetime_bool){ .updatetime = true })
diff --git a/bpf_progs/clatd.c b/bpf_progs/clatd.c
index 8f0ff84..addb02f 100644
--- a/bpf_progs/clatd.c
+++ b/bpf_progs/clatd.c
@@ -55,8 +55,10 @@
 DEFINE_BPF_MAP_GRW(clat_ingress6_map, HASH, ClatIngress6Key, ClatIngress6Value, 16, AID_SYSTEM)
 
 static inline __always_inline int nat64(struct __sk_buff* skb,
-                                        const bool is_ethernet,
-                                        const unsigned kver) {
+                                        const struct rawip_bool rawip,
+                                        const struct kver_uint kver) {
+    const bool is_ethernet = !rawip.rawip;
+
     // Require ethernet dst mac address to be our unicast address.
     if (is_ethernet && (skb->pkt_type != PACKET_HOST)) return TC_ACT_PIPE;
 
@@ -115,7 +117,7 @@
 
     if (proto == IPPROTO_FRAGMENT) {
         // Fragment handling requires bpf_skb_adjust_room which is 4.14+
-        if (kver < KVER_4_14) return TC_ACT_PIPE;
+        if (!KVER_IS_AT_LEAST(kver, 4, 14, 0)) return TC_ACT_PIPE;
 
         // Must have (ethernet and) ipv6 header and ipv6 fragment extension header
         if (data + l2_header_size + sizeof(*ip6) + sizeof(struct frag_hdr) > data_end)
@@ -233,7 +235,7 @@
     //
     // Note: we currently have no TreeHugger coverage for 4.9-T devices (there are no such
     // Pixel or cuttlefish devices), so likely you won't notice for months if this breaks...
-    if (kver >= KVER_4_14 && frag_off != htons(IP_DF)) {
+    if (KVER_IS_AT_LEAST(kver, 4, 14, 0) && frag_off != htons(IP_DF)) {
         // If we're converting an IPv6 Fragment, we need to trim off 8 more bytes
         // We're beyond recovery on error here... but hard to imagine how this could fail.
         if (bpf_skb_adjust_room(skb, -(__s32)sizeof(struct frag_hdr), BPF_ADJ_ROOM_NET, /*flags*/0))
diff --git a/bpf_progs/dscpPolicy.c b/bpf_progs/dscpPolicy.c
index 88b50b5..e845a69 100644
--- a/bpf_progs/dscpPolicy.c
+++ b/bpf_progs/dscpPolicy.c
@@ -222,7 +222,7 @@
 }
 
 DEFINE_BPF_PROG_KVER("schedcls/set_dscp_ether", AID_ROOT, AID_SYSTEM, schedcls_set_dscp_ether,
-                     KVER(5, 15, 0))
+                     KVER_5_15)
 (struct __sk_buff* skb) {
     if (skb->pkt_type != PACKET_HOST) return TC_ACT_PIPE;
 
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index e2e6d02..9017976 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -56,19 +56,21 @@
 // see include/uapi/linux/tcp.h
 #define TCP_FLAG32_OFF 12
 
+#define TCP_FLAG8_OFF (TCP_FLAG32_OFF + 1)
+
 // For maps netd does not need to access
-#define DEFINE_BPF_MAP_NO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries)      \
-    DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries,              \
-                       AID_ROOT, AID_NET_BW_ACCT, 0060, "fs_bpf_net_shared", "", false, \
-                       BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, LOAD_ON_ENG,       \
-                       LOAD_ON_USER, LOAD_ON_USERDEBUG)
+#define DEFINE_BPF_MAP_NO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
+    DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries,         \
+                       AID_ROOT, AID_NET_BW_ACCT, 0060, "fs_bpf_net_shared", "",   \
+                       PRIVATE, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER,              \
+                       LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
 
 // For maps netd only needs read only access to
-#define DEFINE_BPF_MAP_RO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries)         \
-    DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries,                 \
-                       AID_ROOT, AID_NET_BW_ACCT, 0460, "fs_bpf_netd_readonly", "", false, \
-                       BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, LOAD_ON_ENG,       \
-                       LOAD_ON_USER, LOAD_ON_USERDEBUG)
+#define DEFINE_BPF_MAP_RO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries)  \
+    DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries,          \
+                       AID_ROOT, AID_NET_BW_ACCT, 0460, "fs_bpf_netd_readonly", "", \
+                       PRIVATE, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER,               \
+                       LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
 
 // For maps netd needs to be able to read and write
 #define DEFINE_BPF_MAP_RW_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
@@ -87,11 +89,11 @@
 DEFINE_BPF_MAP_RW_NETD(cookie_tag_map, HASH, uint64_t, UidTagValue, COOKIE_UID_MAP_SIZE)
 DEFINE_BPF_MAP_NO_NETD(uid_counterset_map, HASH, uint32_t, uint8_t, UID_COUNTERSET_MAP_SIZE)
 DEFINE_BPF_MAP_NO_NETD(app_uid_stats_map, HASH, uint32_t, StatsValue, APP_STATS_MAP_SIZE)
-DEFINE_BPF_MAP_RW_NETD(stats_map_A, HASH, StatsKey, StatsValue, STATS_MAP_SIZE)
+DEFINE_BPF_MAP_RO_NETD(stats_map_A, HASH, StatsKey, StatsValue, STATS_MAP_SIZE)
 DEFINE_BPF_MAP_RO_NETD(stats_map_B, HASH, StatsKey, StatsValue, STATS_MAP_SIZE)
 DEFINE_BPF_MAP_NO_NETD(iface_stats_map, HASH, uint32_t, StatsValue, IFACE_STATS_MAP_SIZE)
 DEFINE_BPF_MAP_NO_NETD(uid_owner_map, HASH, uint32_t, UidOwnerValue, UID_OWNER_MAP_SIZE)
-DEFINE_BPF_MAP_RW_NETD(uid_permission_map, HASH, uint32_t, uint8_t, UID_OWNER_MAP_SIZE)
+DEFINE_BPF_MAP_RO_NETD(uid_permission_map, HASH, uint32_t, uint8_t, UID_OWNER_MAP_SIZE)
 DEFINE_BPF_MAP_NO_NETD(ingress_discard_map, HASH, IngressDiscardKey, IngressDiscardValue,
                        INGRESS_DISCARD_MAP_SIZE)
 
@@ -100,16 +102,15 @@
 
 // A single-element configuration array, packet tracing is enabled when 'true'.
 DEFINE_BPF_MAP_EXT(packet_trace_enabled_map, ARRAY, uint32_t, bool, 1,
-                   AID_ROOT, AID_SYSTEM, 0060, "fs_bpf_net_shared", "", false,
+                   AID_ROOT, AID_SYSTEM, 0060, "fs_bpf_net_shared", "", PRIVATE,
                    BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG,
-                   IGNORE_ON_USER, LOAD_ON_USERDEBUG)
+                   LOAD_ON_USER, LOAD_ON_USERDEBUG)
 
-// A ring buffer on which packet information is pushed. This map will only be loaded
-// on eng and userdebug devices. User devices won't load this to save memory.
+// A ring buffer on which packet information is pushed.
 DEFINE_BPF_RINGBUF_EXT(packet_trace_ringbuf, PacketTrace, PACKET_TRACE_BUF_SIZE,
-                       AID_ROOT, AID_SYSTEM, 0060, "fs_bpf_net_shared", "", false,
+                       AID_ROOT, AID_SYSTEM, 0060, "fs_bpf_net_shared", "", PRIVATE,
                        BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG,
-                       IGNORE_ON_USER, LOAD_ON_USERDEBUG);
+                       LOAD_ON_USER, LOAD_ON_USERDEBUG);
 
 // iptables xt_bpf programs need to be usable by both netd and netutils_wrappers
 // selinux contexts, because even non-xt_bpf iptables mutations are implemented as
@@ -126,8 +127,8 @@
 // which is loaded into netd and thus runs as netd uid/gid/selinux context)
 #define DEFINE_NETD_BPF_PROG_KVER_RANGE(SECTION_NAME, prog_uid, prog_gid, the_prog, minKV, maxKV) \
     DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog,                               \
-                        minKV, maxKV, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, false,                \
-                        "fs_bpf_netd_readonly", "", false, false, false)
+                        minKV, maxKV, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, MANDATORY,            \
+                        "fs_bpf_netd_readonly", "", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
 
 #define DEFINE_NETD_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv) \
     DEFINE_NETD_BPF_PROG_KVER_RANGE(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, KVER_INF)
@@ -138,8 +139,8 @@
 // programs that only need to be usable by the system server
 #define DEFINE_SYS_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
     DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, KVER_NONE, KVER_INF,  \
-                        BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, false, "fs_bpf_net_shared", \
-                        "", false, false, false)
+                        BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, MANDATORY, \
+                        "fs_bpf_net_shared", "", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
 
 static __always_inline int is_system_uid(uint32_t uid) {
     // MIN_SYSTEM_UID is AID_ROOT == 0, so uint32_t is *always* >= 0
@@ -177,8 +178,8 @@
 #define DEFINE_UPDATE_STATS(the_stats_map, TypeOfKey)                                            \
     static __always_inline inline void update_##the_stats_map(const struct __sk_buff* const skb, \
                                                               const TypeOfKey* const key,        \
-                                                              const bool egress,                 \
-                                                              const unsigned kver) {             \
+                                                              const struct egress_bool egress,   \
+                                                              const struct kver_uint kver) {     \
         StatsValue* value = bpf_##the_stats_map##_lookup_elem(key);                              \
         if (!value) {                                                                            \
             StatsValue newValue = {};                                                            \
@@ -198,7 +199,7 @@
                 packets = (payload + mss - 1) / mss;                                             \
                 bytes = tcp_overhead * packets + payload;                                        \
             }                                                                                    \
-            if (egress) {                                                                        \
+            if (egress.egress) {                                                                 \
                 __sync_fetch_and_add(&value->txPackets, packets);                                \
                 __sync_fetch_and_add(&value->txBytes, bytes);                                    \
             } else {                                                                             \
@@ -218,7 +219,7 @@
                                                          const int L3_off,
                                                          void* const to,
                                                          const int len,
-                                                         const unsigned kver) {
+                                                         const struct kver_uint kver) {
     // 'kver' (here and throughout) is the compile time guaranteed minimum kernel version,
     // ie. we're building (a version of) the bpf program for kver (or newer!) kernels.
     //
@@ -235,16 +236,16 @@
     //
     // For similar reasons this will fail with non-offloaded VLAN tags on < 4.19 kernels,
     // since those extend the ethernet header from 14 to 18 bytes.
-    return kver >= KVER(4, 19, 0)
+    return KVER_IS_AT_LEAST(kver, 4, 19, 0)
         ? bpf_skb_load_bytes_relative(skb, L3_off, to, len, BPF_HDR_START_NET)
         : bpf_skb_load_bytes(skb, L3_off, to, len);
 }
 
 static __always_inline inline void do_packet_tracing(
-        const struct __sk_buff* const skb, const bool egress, const uint32_t uid,
-        const uint32_t tag, const bool enable_tracing, const unsigned kver) {
+        const struct __sk_buff* const skb, const struct egress_bool egress, const uint32_t uid,
+        const uint32_t tag, const bool enable_tracing, const struct kver_uint kver) {
     if (!enable_tracing) return;
-    if (kver < KVER(5, 8, 0)) return;
+    if (!KVER_IS_AT_LEAST(kver, 5, 8, 0)) return;
 
     uint32_t mapKey = 0;
     bool* traceConfig = bpf_packet_trace_enabled_map_lookup_elem(&mapKey);
@@ -270,17 +271,41 @@
         (void)bpf_skb_load_bytes_net(skb, IP6_OFFSET(nexthdr), &proto, sizeof(proto), kver);
         L4_off = sizeof(struct ipv6hdr);
         ipVersion = 6;
+        // skip over a *single* HOPOPTS or DSTOPTS extension header (if present)
+        if (proto == IPPROTO_HOPOPTS || proto == IPPROTO_DSTOPTS) {
+            struct {
+                uint8_t proto, len;
+            } ext_hdr;
+            if (!bpf_skb_load_bytes_net(skb, L4_off, &ext_hdr, sizeof(ext_hdr), kver)) {
+                proto = ext_hdr.proto;
+                L4_off += (ext_hdr.len + 1) * 8;
+            }
+        }
     }
 
     uint8_t flags = 0;
     __be16 sport = 0, dport = 0;
-    if (proto == IPPROTO_TCP && L4_off >= 20) {
-        (void)bpf_skb_load_bytes_net(skb, L4_off + TCP_FLAG32_OFF + 1, &flags, sizeof(flags), kver);
-        (void)bpf_skb_load_bytes_net(skb, L4_off + TCP_OFFSET(source), &sport, sizeof(sport), kver);
-        (void)bpf_skb_load_bytes_net(skb, L4_off + TCP_OFFSET(dest), &dport, sizeof(dport), kver);
-    } else if (proto == IPPROTO_UDP && L4_off >= 20) {
-        (void)bpf_skb_load_bytes_net(skb, L4_off + UDP_OFFSET(source), &sport, sizeof(sport), kver);
-        (void)bpf_skb_load_bytes_net(skb, L4_off + UDP_OFFSET(dest), &dport, sizeof(dport), kver);
+    if (L4_off >= 20) {
+      switch (proto) {
+        case IPPROTO_TCP:
+          (void)bpf_skb_load_bytes_net(skb, L4_off + TCP_FLAG8_OFF, &flags, sizeof(flags), kver);
+          // fallthrough
+        case IPPROTO_DCCP:
+        case IPPROTO_UDP:
+        case IPPROTO_UDPLITE:
+        case IPPROTO_SCTP:
+          // all of these L4 protocols start with be16 src & dst port
+          (void)bpf_skb_load_bytes_net(skb, L4_off + 0, &sport, sizeof(sport), kver);
+          (void)bpf_skb_load_bytes_net(skb, L4_off + 2, &dport, sizeof(dport), kver);
+          break;
+        case IPPROTO_ICMP:
+        case IPPROTO_ICMPV6:
+          // Both IPv4 and IPv6 icmp start with u8 type & code, which we store in the bottom
+          // (ie. second) byte of sport/dport (which are be16s), the top byte is already zero.
+          (void)bpf_skb_load_bytes_net(skb, L4_off + 0, (char *)&sport + 1, 1, kver); //type
+          (void)bpf_skb_load_bytes_net(skb, L4_off + 1, (char *)&dport + 1, 1, kver); //code
+          break;
+      }
     }
 
     pkt->timestampNs = bpf_ktime_get_boot_ns();
@@ -292,7 +317,8 @@
     pkt->sport = sport;
     pkt->dport = dport;
 
-    pkt->egress = egress;
+    pkt->egress = egress.egress;
+    pkt->wakeup = !egress.egress && (skb->mark & 0x80000000);  // Fwmark.ingress_cpu_wakeup
     pkt->ipProto = proto;
     pkt->tcpFlags = flags;
     pkt->ipVersion = ipVersion;
@@ -300,8 +326,9 @@
     bpf_packet_trace_ringbuf_submit(pkt);
 }
 
-static __always_inline inline bool skip_owner_match(struct __sk_buff* skb, bool egress,
-                                                    const unsigned kver) {
+static __always_inline inline bool skip_owner_match(struct __sk_buff* skb,
+                                                    const struct egress_bool egress,
+                                                    const struct kver_uint kver) {
     uint32_t flag = 0;
     if (skb->protocol == htons(ETH_P_IP)) {
         uint8_t proto;
@@ -332,7 +359,7 @@
         return false;
     }
     // Always allow RST's, and additionally allow ingress FINs
-    return flag & (TCP_FLAG_RST | (egress ? 0 : TCP_FLAG_FIN));  // false on read failure
+    return flag & (TCP_FLAG_RST | (egress.egress ? 0 : TCP_FLAG_FIN));  // false on read failure
 }
 
 static __always_inline inline BpfConfig getConfig(uint32_t configKey) {
@@ -346,11 +373,11 @@
 }
 
 static __always_inline inline bool ingress_should_discard(struct __sk_buff* skb,
-                                                          const unsigned kver) {
+                                                          const struct kver_uint kver) {
     // Require 4.19, since earlier kernels don't have bpf_skb_load_bytes_relative() which
     // provides relative to L3 header reads.  Without that we could fetch the wrong bytes.
     // Additionally earlier bpf verifiers are much harder to please.
-    if (kver < KVER(4, 19, 0)) return false;
+    if (!KVER_IS_AT_LEAST(kver, 4, 19, 0)) return false;
 
     IngressDiscardKey k = {};
     if (skb->protocol == htons(ETH_P_IP)) {
@@ -374,13 +401,9 @@
     return true;  // disallowed interface
 }
 
-// DROP_IF_SET is set of rules that DROP if rule is globally enabled, and per-uid bit is set
-#define DROP_IF_SET (STANDBY_MATCH | OEM_DENY_1_MATCH | OEM_DENY_2_MATCH | OEM_DENY_3_MATCH)
-// DROP_IF_UNSET is set of rules that should DROP if globally enabled, and per-uid bit is NOT set
-#define DROP_IF_UNSET (DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH | LOW_POWER_STANDBY_MATCH)
-
 static __always_inline inline int bpf_owner_match(struct __sk_buff* skb, uint32_t uid,
-                                                  bool egress, const unsigned kver) {
+                                                  const struct egress_bool egress,
+                                                  const struct kver_uint kver) {
     if (is_system_uid(uid)) return PASS;
 
     if (skip_owner_match(skb, egress, kver)) return PASS;
@@ -391,14 +414,9 @@
     uint32_t uidRules = uidEntry ? uidEntry->rule : 0;
     uint32_t allowed_iif = uidEntry ? uidEntry->iif : 0;
 
-    // Warning: funky bit-wise arithmetic: in parallel, for all DROP_IF_SET/UNSET rules
-    // check whether the rules are globally enabled, and if so whether the rules are
-    // set/unset for the specific uid.  DROP if that is the case for ANY of the rules.
-    // We achieve this by masking out only the bits/rules we're interested in checking,
-    // and negating (via bit-wise xor) the bits/rules that should drop if unset.
-    if (enabledRules & (DROP_IF_SET | DROP_IF_UNSET) & (uidRules ^ DROP_IF_UNSET)) return DROP;
+    if (isBlockedByUidRules(enabledRules, uidRules)) return DROP;
 
-    if (!egress && skb->ifindex != 1) {
+    if (!egress.egress && skb->ifindex != 1) {
         if (ingress_should_discard(skb, kver)) return DROP;
         if (uidRules & IIF_MATCH) {
             if (allowed_iif && skb->ifindex != allowed_iif) {
@@ -418,8 +436,8 @@
 static __always_inline inline void update_stats_with_config(const uint32_t selectedMap,
                                                             const struct __sk_buff* const skb,
                                                             const StatsKey* const key,
-                                                            const bool egress,
-                                                            const unsigned kver) {
+                                                            const struct egress_bool egress,
+                                                            const struct kver_uint kver) {
     if (selectedMap == SELECT_MAP_A) {
         update_stats_map_A(skb, key, egress, kver);
     } else {
@@ -427,9 +445,10 @@
     }
 }
 
-static __always_inline inline int bpf_traffic_account(struct __sk_buff* skb, bool egress,
+static __always_inline inline int bpf_traffic_account(struct __sk_buff* skb,
+                                                      const struct egress_bool egress,
                                                       const bool enable_tracing,
-                                                      const unsigned kver) {
+                                                      const struct kver_uint kver) {
     uint32_t sock_uid = bpf_get_socket_uid(skb);
     uint64_t cookie = bpf_get_socket_cookie(skb);
     UidTagValue* utag = bpf_cookie_tag_map_lookup_elem(&cookie);
@@ -446,7 +465,7 @@
     // interface is accounted for and subject to usage restrictions.
     // CLAT IPv6 TX sockets are *always* tagged with CLAT uid, see tagSocketAsClat()
     // CLAT daemon receives via an untagged AF_PACKET socket.
-    if (egress && uid == AID_CLAT) return PASS;
+    if (egress.egress && uid == AID_CLAT) return PASS;
 
     int match = bpf_owner_match(skb, sock_uid, egress, kver);
 
@@ -462,7 +481,7 @@
     }
 
     // If an outbound packet is going to be dropped, we do not count that traffic.
-    if (egress && (match == DROP)) return DROP;
+    if (egress.egress && (match == DROP)) return DROP;
 
     StatsKey key = {.uid = uid, .tag = tag, .counterSet = 0, .ifaceIndex = skb->ifindex};
 
@@ -487,42 +506,66 @@
     return match;
 }
 
-DEFINE_BPF_PROG_EXT("cgroupskb/ingress/stats$trace", AID_ROOT, AID_SYSTEM,
-                    bpf_cgroup_ingress_trace, KVER(5, 8, 0), KVER_INF,
-                    BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, false,
-                    "fs_bpf_netd_readonly", "", false, true, false)
+// This program is optional, and enables tracing on Android U+, 5.8+ on user builds.
+DEFINE_BPF_PROG_EXT("cgroupskb/ingress/stats$trace_user", AID_ROOT, AID_SYSTEM,
+                    bpf_cgroup_ingress_trace_user, KVER_5_8, KVER_INF,
+                    BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, OPTIONAL,
+                    "fs_bpf_netd_readonly", "",
+                    IGNORE_ON_ENG, LOAD_ON_USER, IGNORE_ON_USERDEBUG)
 (struct __sk_buff* skb) {
-    return bpf_traffic_account(skb, INGRESS, TRACE_ON, KVER(5, 8, 0));
+    return bpf_traffic_account(skb, INGRESS, TRACE_ON, KVER_5_8);
+}
+
+// This program is required, and enables tracing on Android U+, 5.8+, userdebug/eng.
+DEFINE_BPF_PROG_EXT("cgroupskb/ingress/stats$trace", AID_ROOT, AID_SYSTEM,
+                    bpf_cgroup_ingress_trace, KVER_5_8, KVER_INF,
+                    BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, MANDATORY,
+                    "fs_bpf_netd_readonly", "",
+                    LOAD_ON_ENG, IGNORE_ON_USER, LOAD_ON_USERDEBUG)
+(struct __sk_buff* skb) {
+    return bpf_traffic_account(skb, INGRESS, TRACE_ON, KVER_5_8);
 }
 
 DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/ingress/stats$4_19", AID_ROOT, AID_SYSTEM,
-                                bpf_cgroup_ingress_4_19, KVER(4, 19, 0), KVER_INF)
+                                bpf_cgroup_ingress_4_19, KVER_4_19, KVER_INF)
 (struct __sk_buff* skb) {
-    return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER(4, 19, 0));
+    return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER_4_19);
 }
 
 DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/ingress/stats$4_14", AID_ROOT, AID_SYSTEM,
-                                bpf_cgroup_ingress_4_14, KVER_NONE, KVER(4, 19, 0))
+                                bpf_cgroup_ingress_4_14, KVER_NONE, KVER_4_19)
 (struct __sk_buff* skb) {
     return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER_NONE);
 }
 
-DEFINE_BPF_PROG_EXT("cgroupskb/egress/stats$trace", AID_ROOT, AID_SYSTEM,
-                    bpf_cgroup_egress_trace, KVER(5, 8, 0), KVER_INF,
-                    BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, false,
-                    "fs_bpf_netd_readonly", "", false, true, false)
+// This program is optional, and enables tracing on Android U+, 5.8+ on user builds.
+DEFINE_BPF_PROG_EXT("cgroupskb/egress/stats$trace_user", AID_ROOT, AID_SYSTEM,
+                    bpf_cgroup_egress_trace_user, KVER_5_8, KVER_INF,
+                    BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, OPTIONAL,
+                    "fs_bpf_netd_readonly", "",
+                    LOAD_ON_ENG, IGNORE_ON_USER, LOAD_ON_USERDEBUG)
 (struct __sk_buff* skb) {
-    return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER(5, 8, 0));
+    return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER_5_8);
+}
+
+// This program is required, and enables tracing on Android U+, 5.8+, userdebug/eng.
+DEFINE_BPF_PROG_EXT("cgroupskb/egress/stats$trace", AID_ROOT, AID_SYSTEM,
+                    bpf_cgroup_egress_trace, KVER_5_8, KVER_INF,
+                    BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, MANDATORY,
+                    "fs_bpf_netd_readonly", "",
+                    LOAD_ON_ENG, IGNORE_ON_USER, LOAD_ON_USERDEBUG)
+(struct __sk_buff* skb) {
+    return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER_5_8);
 }
 
 DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/egress/stats$4_19", AID_ROOT, AID_SYSTEM,
-                                bpf_cgroup_egress_4_19, KVER(4, 19, 0), KVER_INF)
+                                bpf_cgroup_egress_4_19, KVER_4_19, KVER_INF)
 (struct __sk_buff* skb) {
-    return bpf_traffic_account(skb, EGRESS, TRACE_OFF, KVER(4, 19, 0));
+    return bpf_traffic_account(skb, EGRESS, TRACE_OFF, KVER_4_19);
 }
 
 DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/egress/stats$4_14", AID_ROOT, AID_SYSTEM,
-                                bpf_cgroup_egress_4_14, KVER_NONE, KVER(4, 19, 0))
+                                bpf_cgroup_egress_4_14, KVER_NONE, KVER_4_19)
 (struct __sk_buff* skb) {
     return bpf_traffic_account(skb, EGRESS, TRACE_OFF, KVER_NONE);
 }
@@ -597,9 +640,7 @@
     return BPF_NOMATCH;
 }
 
-DEFINE_NETD_BPF_PROG_KVER("cgroupsock/inet/create", AID_ROOT, AID_ROOT, inet_socket_create,
-                          KVER(4, 14, 0))
-(struct bpf_sock* sk) {
+static __always_inline inline uint8_t get_app_permissions() {
     uint64_t gid_uid = bpf_get_current_uid_gid();
     /*
      * A given app is guaranteed to have the same app ID in all the profiles in
@@ -609,13 +650,15 @@
      */
     uint32_t appId = (gid_uid & 0xffffffff) % AID_USER_OFFSET;  // == PER_USER_RANGE == 100000
     uint8_t* permissions = bpf_uid_permission_map_lookup_elem(&appId);
-    if (!permissions) {
-        // UID not in map. Default to just INTERNET permission.
-        return 1;
-    }
+    // if UID not in map, then default to just INTERNET permission.
+    return permissions ? *permissions : BPF_PERMISSION_INTERNET;
+}
 
+DEFINE_NETD_BPF_PROG_KVER("cgroupsock/inet/create", AID_ROOT, AID_ROOT, inet_socket_create,
+                          KVER_4_14)
+(struct bpf_sock* sk) {
     // A return value of 1 means allow, everything else means deny.
-    return (*permissions & BPF_PERMISSION_INTERNET) == BPF_PERMISSION_INTERNET;
+    return (get_app_permissions() & BPF_PERMISSION_INTERNET) ? 1 : 0;
 }
 
 LICENSE("Apache 2.0");
diff --git a/bpf_progs/netd.h b/bpf_progs/netd.h
index 836e998..dd27bf9 100644
--- a/bpf_progs/netd.h
+++ b/bpf_progs/netd.h
@@ -81,7 +81,8 @@
   __be16 sport;
   __be16 dport;
 
-  bool egress;
+  bool egress:1,
+       wakeup:1;
   uint8_t ipProto;
   uint8_t tcpFlags;
   uint8_t ipVersion; // 4=IPv4, 6=IPv6, 0=unknown
@@ -234,3 +235,17 @@
 #define CURRENT_STATS_MAP_CONFIGURATION_KEY 1
 
 #undef STRUCT_SIZE
+
+// DROP_IF_SET is set of rules that DROP if rule is globally enabled, and per-uid bit is set
+#define DROP_IF_SET (STANDBY_MATCH | OEM_DENY_1_MATCH | OEM_DENY_2_MATCH | OEM_DENY_3_MATCH)
+// DROP_IF_UNSET is set of rules that should DROP if globally enabled, and per-uid bit is NOT set
+#define DROP_IF_UNSET (DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH | LOW_POWER_STANDBY_MATCH)
+
+// Warning: funky bit-wise arithmetic: in parallel, for all DROP_IF_SET/UNSET rules
+// check whether the rules are globally enabled, and if so whether the rules are
+// set/unset for the specific uid.  DROP if that is the case for ANY of the rules.
+// We achieve this by masking out only the bits/rules we're interested in checking,
+// and negating (via bit-wise xor) the bits/rules that should drop if unset.
+static inline bool isBlockedByUidRules(BpfConfig enabledRules, uint32_t uidRules) {
+    return enabledRules & (DROP_IF_SET | DROP_IF_UNSET) & (uidRules ^ DROP_IF_UNSET);
+}
diff --git a/bpf_progs/offload.c b/bpf_progs/offload.c
index c752779..35b8eea 100644
--- a/bpf_progs/offload.c
+++ b/bpf_progs/offload.c
@@ -124,8 +124,12 @@
 DEFINE_BPF_MAP_GRW(tether_upstream6_map, HASH, TetherUpstream6Key, Tether6Value, 64,
                    TETHERING_GID)
 
-static inline __always_inline int do_forward6(struct __sk_buff* skb, const bool is_ethernet,
-        const bool downstream, const unsigned kver) {
+static inline __always_inline int do_forward6(struct __sk_buff* skb,
+                                              const struct rawip_bool rawip,
+                                              const struct stream_bool stream,
+                                              const struct kver_uint kver) {
+    const bool is_ethernet = !rawip.rawip;
+
     // Must be meta-ethernet IPv6 frame
     if (skb->protocol != htons(ETH_P_IPV6)) return TC_ACT_PIPE;
 
@@ -184,7 +188,7 @@
         TC_PUNT(NON_GLOBAL_DST);
 
     // In the upstream direction do not forward traffic within the same /64 subnet.
-    if (!downstream && (src32 == dst32) && (ip6->saddr.s6_addr32[1] == ip6->daddr.s6_addr32[1]))
+    if (!stream.down && (src32 == dst32) && (ip6->saddr.s6_addr32[1] == ip6->daddr.s6_addr32[1]))
         TC_PUNT(LOCAL_SRC_DST);
 
     TetherDownstream6Key kd = {
@@ -196,15 +200,15 @@
             .iif = skb->ifindex,
             .src64 = 0,
     };
-    if (is_ethernet) __builtin_memcpy(downstream ? kd.dstMac : ku.dstMac, eth->h_dest, ETH_ALEN);
+    if (is_ethernet) __builtin_memcpy(stream.down ? kd.dstMac : ku.dstMac, eth->h_dest, ETH_ALEN);
 
-    Tether6Value* v = downstream ? bpf_tether_downstream6_map_lookup_elem(&kd)
-                                 : bpf_tether_upstream6_map_lookup_elem(&ku);
+    Tether6Value* v = stream.down ? bpf_tether_downstream6_map_lookup_elem(&kd)
+                                  : bpf_tether_upstream6_map_lookup_elem(&ku);
 
     // If we don't find any offload information then simply let the core stack handle it...
     if (!v) return TC_ACT_PIPE;
 
-    uint32_t stat_and_limit_k = downstream ? skb->ifindex : v->oif;
+    uint32_t stat_and_limit_k = stream.down ? skb->ifindex : v->oif;
 
     TetherStatsValue* stat_v = bpf_tether_stats_map_lookup_elem(&stat_and_limit_k);
 
@@ -249,7 +253,7 @@
         // We do this even if TX interface is RAWIP and thus does not need an ethernet header,
         // because this is easier and the kernel will strip extraneous ethernet header.
         if (bpf_skb_change_head(skb, sizeof(struct ethhdr), /*flags*/ 0)) {
-            __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1);
+            __sync_fetch_and_add(stream.down ? &stat_v->rxErrors : &stat_v->txErrors, 1);
             TC_PUNT(CHANGE_HEAD_FAILED);
         }
 
@@ -261,7 +265,7 @@
 
         // I do not believe this can ever happen, but keep the verifier happy...
         if (data + sizeof(struct ethhdr) + sizeof(*ip6) > data_end) {
-            __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1);
+            __sync_fetch_and_add(stream.down ? &stat_v->rxErrors : &stat_v->txErrors, 1);
             TC_DROP(TOO_SHORT);
         }
     };
@@ -281,8 +285,8 @@
     // (-ENOTSUPP) if it isn't.
     bpf_csum_update(skb, 0xFFFF - ntohs(old_hl) + ntohs(new_hl));
 
-    __sync_fetch_and_add(downstream ? &stat_v->rxPackets : &stat_v->txPackets, packets);
-    __sync_fetch_and_add(downstream ? &stat_v->rxBytes : &stat_v->txBytes, L3_bytes);
+    __sync_fetch_and_add(stream.down ? &stat_v->rxPackets : &stat_v->txPackets, packets);
+    __sync_fetch_and_add(stream.down ? &stat_v->rxBytes : &stat_v->txBytes, L3_bytes);
 
     // Overwrite any mac header with the new one
     // For a rawip tx interface it will simply be a bunch of zeroes and later stripped.
@@ -324,26 +328,26 @@
 //
 // Hence, these mandatory (must load successfully) implementations for 4.14+ kernels:
 DEFINE_BPF_PROG_KVER("schedcls/tether_downstream6_rawip$4_14", TETHERING_UID, TETHERING_GID,
-                     sched_cls_tether_downstream6_rawip_4_14, KVER(4, 14, 0))
+                     sched_cls_tether_downstream6_rawip_4_14, KVER_4_14)
 (struct __sk_buff* skb) {
-    return do_forward6(skb, RAWIP, DOWNSTREAM, KVER(4, 14, 0));
+    return do_forward6(skb, RAWIP, DOWNSTREAM, KVER_4_14);
 }
 
 DEFINE_BPF_PROG_KVER("schedcls/tether_upstream6_rawip$4_14", TETHERING_UID, TETHERING_GID,
-                     sched_cls_tether_upstream6_rawip_4_14, KVER(4, 14, 0))
+                     sched_cls_tether_upstream6_rawip_4_14, KVER_4_14)
 (struct __sk_buff* skb) {
-    return do_forward6(skb, RAWIP, UPSTREAM, KVER(4, 14, 0));
+    return do_forward6(skb, RAWIP, UPSTREAM, KVER_4_14);
 }
 
 // and define no-op stubs for pre-4.14 kernels.
 DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream6_rawip$stub", TETHERING_UID, TETHERING_GID,
-                           sched_cls_tether_downstream6_rawip_stub, KVER_NONE, KVER(4, 14, 0))
+                           sched_cls_tether_downstream6_rawip_stub, KVER_NONE, KVER_4_14)
 (struct __sk_buff* skb) {
     return TC_ACT_PIPE;
 }
 
 DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream6_rawip$stub", TETHERING_UID, TETHERING_GID,
-                           sched_cls_tether_upstream6_rawip_stub, KVER_NONE, KVER(4, 14, 0))
+                           sched_cls_tether_upstream6_rawip_stub, KVER_NONE, KVER_4_14)
 (struct __sk_buff* skb) {
     return TC_ACT_PIPE;
 }
@@ -356,9 +360,10 @@
 
 static inline __always_inline int do_forward4_bottom(struct __sk_buff* skb,
         const int l2_header_size, void* data, const void* data_end,
-        struct ethhdr* eth, struct iphdr* ip, const bool is_ethernet,
-        const bool downstream, const bool updatetime, const bool is_tcp,
-        const unsigned kver) {
+        struct ethhdr* eth, struct iphdr* ip, const struct rawip_bool rawip,
+        const struct stream_bool stream, const struct updatetime_bool updatetime,
+        const bool is_tcp, const struct kver_uint kver) {
+    const bool is_ethernet = !rawip.rawip;
     struct tcphdr* tcph = is_tcp ? (void*)(ip + 1) : NULL;
     struct udphdr* udph = is_tcp ? NULL : (void*)(ip + 1);
 
@@ -416,13 +421,13 @@
     };
     if (is_ethernet) __builtin_memcpy(k.dstMac, eth->h_dest, ETH_ALEN);
 
-    Tether4Value* v = downstream ? bpf_tether_downstream4_map_lookup_elem(&k)
-                                 : bpf_tether_upstream4_map_lookup_elem(&k);
+    Tether4Value* v = stream.down ? bpf_tether_downstream4_map_lookup_elem(&k)
+                                  : bpf_tether_upstream4_map_lookup_elem(&k);
 
     // If we don't find any offload information then simply let the core stack handle it...
     if (!v) return TC_ACT_PIPE;
 
-    uint32_t stat_and_limit_k = downstream ? skb->ifindex : v->oif;
+    uint32_t stat_and_limit_k = stream.down ? skb->ifindex : v->oif;
 
     TetherStatsValue* stat_v = bpf_tether_stats_map_lookup_elem(&stat_and_limit_k);
 
@@ -467,7 +472,7 @@
         // We do this even if TX interface is RAWIP and thus does not need an ethernet header,
         // because this is easier and the kernel will strip extraneous ethernet header.
         if (bpf_skb_change_head(skb, sizeof(struct ethhdr), /*flags*/ 0)) {
-            __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1);
+            __sync_fetch_and_add(stream.down ? &stat_v->rxErrors : &stat_v->txErrors, 1);
             TC_PUNT(CHANGE_HEAD_FAILED);
         }
 
@@ -481,7 +486,7 @@
 
         // I do not believe this can ever happen, but keep the verifier happy...
         if (data + sizeof(struct ethhdr) + sizeof(*ip) + (is_tcp ? sizeof(*tcph) : sizeof(*udph)) > data_end) {
-            __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1);
+            __sync_fetch_and_add(stream.down ? &stat_v->rxErrors : &stat_v->txErrors, 1);
             TC_DROP(TOO_SHORT);
         }
     };
@@ -533,10 +538,10 @@
 
     // This requires the bpf_ktime_get_boot_ns() helper which was added in 5.8,
     // and backported to all Android Common Kernel 4.14+ trees.
-    if (updatetime) v->last_used = bpf_ktime_get_boot_ns();
+    if (updatetime.updatetime) v->last_used = bpf_ktime_get_boot_ns();
 
-    __sync_fetch_and_add(downstream ? &stat_v->rxPackets : &stat_v->txPackets, packets);
-    __sync_fetch_and_add(downstream ? &stat_v->rxBytes : &stat_v->txBytes, L3_bytes);
+    __sync_fetch_and_add(stream.down ? &stat_v->rxPackets : &stat_v->txPackets, packets);
+    __sync_fetch_and_add(stream.down ? &stat_v->rxBytes : &stat_v->txBytes, L3_bytes);
 
     // Redirect to forwarded interface.
     //
@@ -547,8 +552,13 @@
     return bpf_redirect(v->oif, 0 /* this is effectively BPF_F_EGRESS */);
 }
 
-static inline __always_inline int do_forward4(struct __sk_buff* skb, const bool is_ethernet,
-        const bool downstream, const bool updatetime, const unsigned kver) {
+static inline __always_inline int do_forward4(struct __sk_buff* skb,
+                                              const struct rawip_bool rawip,
+                                              const struct stream_bool stream,
+                                              const struct updatetime_bool updatetime,
+                                              const struct kver_uint kver) {
+    const bool is_ethernet = !rawip.rawip;
+
     // Require ethernet dst mac address to be our unicast address.
     if (is_ethernet && (skb->pkt_type != PACKET_HOST)) return TC_ACT_PIPE;
 
@@ -606,16 +616,16 @@
     // in such a situation we can only support TCP.  This also has the added nice benefit of
     // using a separate error counter, and thus making it obvious which version of the program
     // is loaded.
-    if (!updatetime && ip->protocol != IPPROTO_TCP) TC_PUNT(NON_TCP);
+    if (!updatetime.updatetime && ip->protocol != IPPROTO_TCP) TC_PUNT(NON_TCP);
 
     // We do not support offloading anything besides IPv4 TCP and UDP, due to need for NAT,
     // but no need to check this if !updatetime due to check immediately above.
-    if (updatetime && (ip->protocol != IPPROTO_TCP) && (ip->protocol != IPPROTO_UDP))
+    if (updatetime.updatetime && (ip->protocol != IPPROTO_TCP) && (ip->protocol != IPPROTO_UDP))
         TC_PUNT(NON_TCP_UDP);
 
     // We want to make sure that the compiler will, in the !updatetime case, entirely optimize
     // out all the non-tcp logic.  Also note that at this point is_udp === !is_tcp.
-    const bool is_tcp = !updatetime || (ip->protocol == IPPROTO_TCP);
+    const bool is_tcp = !updatetime.updatetime || (ip->protocol == IPPROTO_TCP);
 
     // This is a bit of a hack to make things easier on the bpf verifier.
     // (In particular I believe the Linux 4.14 kernel's verifier can get confused later on about
@@ -636,37 +646,37 @@
     // if the underlying requisite kernel support (bpf_ktime_get_boot_ns) was backported.
     if (is_tcp) {
       return do_forward4_bottom(skb, l2_header_size, data, data_end, eth, ip,
-                                is_ethernet, downstream, updatetime, /* is_tcp */ true, kver);
+                                rawip, stream, updatetime, /* is_tcp */ true, kver);
     } else {
       return do_forward4_bottom(skb, l2_header_size, data, data_end, eth, ip,
-                                is_ethernet, downstream, updatetime, /* is_tcp */ false, kver);
+                                rawip, stream, updatetime, /* is_tcp */ false, kver);
     }
 }
 
 // Full featured (required) implementations for 5.8+ kernels (these are S+ by definition)
 
 DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_rawip$5_8", TETHERING_UID, TETHERING_GID,
-                     sched_cls_tether_downstream4_rawip_5_8, KVER(5, 8, 0))
+                     sched_cls_tether_downstream4_rawip_5_8, KVER_5_8)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER(5, 8, 0));
+    return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER_5_8);
 }
 
 DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_rawip$5_8", TETHERING_UID, TETHERING_GID,
-                     sched_cls_tether_upstream4_rawip_5_8, KVER(5, 8, 0))
+                     sched_cls_tether_upstream4_rawip_5_8, KVER_5_8)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER(5, 8, 0));
+    return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER_5_8);
 }
 
 DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_ether$5_8", TETHERING_UID, TETHERING_GID,
-                     sched_cls_tether_downstream4_ether_5_8, KVER(5, 8, 0))
+                     sched_cls_tether_downstream4_ether_5_8, KVER_5_8)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER(5, 8, 0));
+    return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER_5_8);
 }
 
 DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_ether$5_8", TETHERING_UID, TETHERING_GID,
-                     sched_cls_tether_upstream4_ether_5_8, KVER(5, 8, 0))
+                     sched_cls_tether_upstream4_ether_5_8, KVER_5_8)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER(5, 8, 0));
+    return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER_5_8);
 }
 
 // Full featured (optional) implementations for 4.14-S, 4.19-S & 5.4-S kernels
@@ -675,33 +685,33 @@
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$opt",
                                     TETHERING_UID, TETHERING_GID,
                                     sched_cls_tether_downstream4_rawip_opt,
-                                    KVER(4, 14, 0), KVER(5, 8, 0))
+                                    KVER_4_14, KVER_5_8)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER(4, 14, 0));
+    return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER_4_14);
 }
 
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$opt",
                                     TETHERING_UID, TETHERING_GID,
                                     sched_cls_tether_upstream4_rawip_opt,
-                                    KVER(4, 14, 0), KVER(5, 8, 0))
+                                    KVER_4_14, KVER_5_8)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER(4, 14, 0));
+    return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER_4_14);
 }
 
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$opt",
                                     TETHERING_UID, TETHERING_GID,
                                     sched_cls_tether_downstream4_ether_opt,
-                                    KVER(4, 14, 0), KVER(5, 8, 0))
+                                    KVER_4_14, KVER_5_8)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER(4, 14, 0));
+    return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER_4_14);
 }
 
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$opt",
                                     TETHERING_UID, TETHERING_GID,
                                     sched_cls_tether_upstream4_ether_opt,
-                                    KVER(4, 14, 0), KVER(5, 8, 0))
+                                    KVER_4_14, KVER_5_8)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER(4, 14, 0));
+    return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER_4_14);
 }
 
 // Partial (TCP-only: will not update 'last_used' field) implementations for 4.14+ kernels.
@@ -719,15 +729,15 @@
 // RAWIP: Required for 5.4-R kernels -- which always support bpf_skb_change_head().
 
 DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$5_4", TETHERING_UID, TETHERING_GID,
-                           sched_cls_tether_downstream4_rawip_5_4, KVER(5, 4, 0), KVER(5, 8, 0))
+                           sched_cls_tether_downstream4_rawip_5_4, KVER_5_4, KVER_5_8)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, RAWIP, DOWNSTREAM, NO_UPDATETIME, KVER(5, 4, 0));
+    return do_forward4(skb, RAWIP, DOWNSTREAM, NO_UPDATETIME, KVER_5_4);
 }
 
 DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$5_4", TETHERING_UID, TETHERING_GID,
-                           sched_cls_tether_upstream4_rawip_5_4, KVER(5, 4, 0), KVER(5, 8, 0))
+                           sched_cls_tether_upstream4_rawip_5_4, KVER_5_4, KVER_5_8)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, RAWIP, UPSTREAM, NO_UPDATETIME, KVER(5, 4, 0));
+    return do_forward4(skb, RAWIP, UPSTREAM, NO_UPDATETIME, KVER_5_4);
 }
 
 // RAWIP: Optional for 4.14/4.19 (R) kernels -- which support bpf_skb_change_head().
@@ -736,31 +746,31 @@
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$4_14",
                                     TETHERING_UID, TETHERING_GID,
                                     sched_cls_tether_downstream4_rawip_4_14,
-                                    KVER(4, 14, 0), KVER(5, 4, 0))
+                                    KVER_4_14, KVER_5_4)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, RAWIP, DOWNSTREAM, NO_UPDATETIME, KVER(4, 14, 0));
+    return do_forward4(skb, RAWIP, DOWNSTREAM, NO_UPDATETIME, KVER_4_14);
 }
 
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$4_14",
                                     TETHERING_UID, TETHERING_GID,
                                     sched_cls_tether_upstream4_rawip_4_14,
-                                    KVER(4, 14, 0), KVER(5, 4, 0))
+                                    KVER_4_14, KVER_5_4)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, RAWIP, UPSTREAM, NO_UPDATETIME, KVER(4, 14, 0));
+    return do_forward4(skb, RAWIP, UPSTREAM, NO_UPDATETIME, KVER_4_14);
 }
 
 // ETHER: Required for 4.14-Q/R, 4.19-Q/R & 5.4-R kernels.
 
 DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$4_14", TETHERING_UID, TETHERING_GID,
-                           sched_cls_tether_downstream4_ether_4_14, KVER(4, 14, 0), KVER(5, 8, 0))
+                           sched_cls_tether_downstream4_ether_4_14, KVER_4_14, KVER_5_8)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, ETHER, DOWNSTREAM, NO_UPDATETIME, KVER(4, 14, 0));
+    return do_forward4(skb, ETHER, DOWNSTREAM, NO_UPDATETIME, KVER_4_14);
 }
 
 DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$4_14", TETHERING_UID, TETHERING_GID,
-                           sched_cls_tether_upstream4_ether_4_14, KVER(4, 14, 0), KVER(5, 8, 0))
+                           sched_cls_tether_upstream4_ether_4_14, KVER_4_14, KVER_5_8)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, ETHER, UPSTREAM, NO_UPDATETIME, KVER(4, 14, 0));
+    return do_forward4(skb, ETHER, UPSTREAM, NO_UPDATETIME, KVER_4_14);
 }
 
 // Placeholder (no-op) implementations for older Q kernels
@@ -768,13 +778,13 @@
 // RAWIP: 4.9-P/Q, 4.14-P/Q & 4.19-Q kernels -- without bpf_skb_change_head() for tc programs
 
 DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$stub", TETHERING_UID, TETHERING_GID,
-                           sched_cls_tether_downstream4_rawip_stub, KVER_NONE, KVER(5, 4, 0))
+                           sched_cls_tether_downstream4_rawip_stub, KVER_NONE, KVER_5_4)
 (struct __sk_buff* skb) {
     return TC_ACT_PIPE;
 }
 
 DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$stub", TETHERING_UID, TETHERING_GID,
-                           sched_cls_tether_upstream4_rawip_stub, KVER_NONE, KVER(5, 4, 0))
+                           sched_cls_tether_upstream4_rawip_stub, KVER_NONE, KVER_5_4)
 (struct __sk_buff* skb) {
     return TC_ACT_PIPE;
 }
@@ -782,13 +792,13 @@
 // ETHER: 4.9-P/Q kernel
 
 DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$stub", TETHERING_UID, TETHERING_GID,
-                           sched_cls_tether_downstream4_ether_stub, KVER_NONE, KVER(4, 14, 0))
+                           sched_cls_tether_downstream4_ether_stub, KVER_NONE, KVER_4_14)
 (struct __sk_buff* skb) {
     return TC_ACT_PIPE;
 }
 
 DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$stub", TETHERING_UID, TETHERING_GID,
-                           sched_cls_tether_upstream4_ether_stub, KVER_NONE, KVER(4, 14, 0))
+                           sched_cls_tether_upstream4_ether_stub, KVER_NONE, KVER_4_14)
 (struct __sk_buff* skb) {
     return TC_ACT_PIPE;
 }
@@ -797,17 +807,18 @@
 
 DEFINE_BPF_MAP_GRW(tether_dev_map, DEVMAP_HASH, uint32_t, uint32_t, 64, TETHERING_GID)
 
-static inline __always_inline int do_xdp_forward6(struct xdp_md *ctx, const bool is_ethernet,
-        const bool downstream) {
+static inline __always_inline int do_xdp_forward6(struct xdp_md *ctx, const struct rawip_bool rawip,
+        const struct stream_bool stream) {
     return XDP_PASS;
 }
 
-static inline __always_inline int do_xdp_forward4(struct xdp_md *ctx, const bool is_ethernet,
-        const bool downstream) {
+static inline __always_inline int do_xdp_forward4(struct xdp_md *ctx, const struct rawip_bool rawip,
+        const struct stream_bool stream) {
     return XDP_PASS;
 }
 
-static inline __always_inline int do_xdp_forward_ether(struct xdp_md *ctx, const bool downstream) {
+static inline __always_inline int do_xdp_forward_ether(struct xdp_md *ctx,
+                                                       const struct stream_bool stream) {
     const void* data = (void*)(long)ctx->data;
     const void* data_end = (void*)(long)ctx->data_end;
     const struct ethhdr* eth = data;
@@ -816,15 +827,16 @@
     if ((void*)(eth + 1) > data_end) return XDP_PASS;
 
     if (eth->h_proto == htons(ETH_P_IPV6))
-        return do_xdp_forward6(ctx, ETHER, downstream);
+        return do_xdp_forward6(ctx, ETHER, stream);
     if (eth->h_proto == htons(ETH_P_IP))
-        return do_xdp_forward4(ctx, ETHER, downstream);
+        return do_xdp_forward4(ctx, ETHER, stream);
 
     // Anything else we don't know how to handle...
     return XDP_PASS;
 }
 
-static inline __always_inline int do_xdp_forward_rawip(struct xdp_md *ctx, const bool downstream) {
+static inline __always_inline int do_xdp_forward_rawip(struct xdp_md *ctx,
+                                                       const struct stream_bool stream) {
     const void* data = (void*)(long)ctx->data;
     const void* data_end = (void*)(long)ctx->data_end;
 
@@ -832,15 +844,15 @@
     if (data_end - data < 1) return XDP_PASS;
     const uint8_t v = (*(uint8_t*)data) >> 4;
 
-    if (v == 6) return do_xdp_forward6(ctx, RAWIP, downstream);
-    if (v == 4) return do_xdp_forward4(ctx, RAWIP, downstream);
+    if (v == 6) return do_xdp_forward6(ctx, RAWIP, stream);
+    if (v == 4) return do_xdp_forward4(ctx, RAWIP, stream);
 
     // Anything else we don't know how to handle...
     return XDP_PASS;
 }
 
 #define DEFINE_XDP_PROG(str, func) \
-    DEFINE_BPF_PROG_KVER(str, TETHERING_UID, TETHERING_GID, func, KVER(5, 9, 0))(struct xdp_md *ctx)
+    DEFINE_BPF_PROG_KVER(str, TETHERING_UID, TETHERING_GID, func, KVER_5_9)(struct xdp_md *ctx)
 
 DEFINE_XDP_PROG("xdp/tether_downstream_ether",
                  xdp_tether_downstream_ether) {
diff --git a/bpf_progs/test.c b/bpf_progs/test.c
index 68469c8..70b08b7 100644
--- a/bpf_progs/test.c
+++ b/bpf_progs/test.c
@@ -49,7 +49,7 @@
 DEFINE_BPF_MAP_GRW(bitmap, ARRAY, int, uint64_t, 2, TETHERING_GID)
 
 DEFINE_BPF_PROG_KVER("xdp/drop_ipv4_udp_ether", TETHERING_UID, TETHERING_GID,
-                      xdp_test, KVER(5, 9, 0))
+                      xdp_test, KVER_5_9)
 (struct xdp_md *ctx) {
     void *data = (void *)(long)ctx->data;
     void *data_end = (void *)(long)ctx->data_end;
diff --git a/clatd/.clang-format b/clatd/.clang-format
new file mode 100644
index 0000000..f1debbd
--- /dev/null
+++ b/clatd/.clang-format
@@ -0,0 +1,8 @@
+BasedOnStyle: Google
+AlignConsecutiveAssignments: true
+AlignEscapedNewlines: Right
+ColumnLimit: 100
+CommentPragmas: NOLINT:.*
+ContinuationIndentWidth: 2
+Cpp11BracedListStyle: false
+TabWidth: 2
diff --git a/clatd/Android.bp b/clatd/Android.bp
new file mode 100644
index 0000000..595c6b9
--- /dev/null
+++ b/clatd/Android.bp
@@ -0,0 +1,118 @@
+package {
+    default_applicable_licenses: ["external_android-clat_license"],
+}
+
+// Added automatically by a large-scale-change
+//
+// large-scale-change included anything that looked like it might be a license
+// text as a license_text. e.g. LICENSE, NOTICE, COPYING etc.
+//
+// Please consider removing redundant or irrelevant files from 'license_text:'.
+// See: http://go/android-license-faq
+license {
+    name: "external_android-clat_license",
+    visibility: [":__subpackages__"],
+    license_kinds: [
+        "SPDX-license-identifier-Apache-2.0",
+    ],
+    license_text: [
+        "LICENSE",
+        "NOTICE",
+    ],
+}
+
+cc_defaults {
+    name: "clatd_defaults",
+
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wunused-parameter",
+
+        // Bug: http://b/33566695
+        "-Wno-address-of-packed-member",
+    ],
+}
+
+// Code used both by the daemon and by unit tests.
+filegroup {
+    name: "clatd_common",
+    srcs: [
+        "clatd.c",
+        "dump.c",
+        "icmp.c",
+        "ipv4.c",
+        "ipv6.c",
+        "logging.c",
+        "translate.c",
+    ],
+}
+
+// The clat daemon.
+cc_binary {
+    name: "clatd",
+    defaults: ["clatd_defaults"],
+    srcs: [
+        ":clatd_common",
+        "main.c"
+    ],
+    static_libs: [
+        "libip_checksum",
+    ],
+    shared_libs: [
+        "liblog",
+    ],
+    relative_install_path: "for-system",
+
+    // Static libc++ for smaller apex size while shipping clatd in the mainline module.
+    // See b/213123047
+    stl: "libc++_static",
+
+    // Only enable clang-tidy for the daemon, not the tests, because enabling it for the
+    // tests substantially increases build/compile cycle times and doesn't really provide a
+    // security benefit.
+    tidy: true,
+    tidy_checks: [
+        "-*",
+        "cert-*",
+        "clang-analyzer-security*",
+        // b/2043314, warnings on memcpy_s, memset_s, snprintf_s calls
+        // are blocking the migration from gnu99 to gnu11.
+        // Until those warnings are fixed, disable these checks.
+        "-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling",
+        "android-*",
+    ],
+    tidy_checks_as_errors: [
+        "clang-analyzer-security*",
+        "cert-*",
+        "android-*",
+    ],
+
+    apex_available: [
+        "com.android.tethering",
+        "//apex_available:platform",
+    ],
+    min_sdk_version: "30",
+}
+
+// Unit tests.
+cc_test {
+    name: "clatd_test",
+    defaults: ["clatd_defaults"],
+    srcs: [
+        ":clatd_common",
+        "clatd_test.cpp"
+    ],
+    static_libs: [
+        "libbase",
+        "libip_checksum",
+        "libnetd_test_tun_interface",
+    ],
+    shared_libs: [
+        "libcutils",
+        "liblog",
+        "libnetutils",
+    ],
+    test_suites: ["device-tests"],
+    require_root: true,
+}
diff --git a/clatd/BUGS b/clatd/BUGS
new file mode 100644
index 0000000..24e6639
--- /dev/null
+++ b/clatd/BUGS
@@ -0,0 +1,5 @@
+known problems/assumptions:
+ - does not handle protocols other than ICMP, UDP, TCP and GRE/ESP
+ - assumes the handset has its own (routed) /64 ipv6 subnet
+ - assumes the /128 ipv6 subnet it generates can use the nat64 gateway
+ - assumes the nat64 gateway has the ipv4 address in the last 32 bits of the ipv6 address (that it uses a /96 plat subnet)
diff --git a/clatd/LICENSE b/clatd/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/clatd/LICENSE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
diff --git a/clatd/METADATA b/clatd/METADATA
new file mode 100644
index 0000000..d97975c
--- /dev/null
+++ b/clatd/METADATA
@@ -0,0 +1,3 @@
+third_party {
+  license_type: NOTICE
+}
diff --git a/clatd/MODULE_LICENSE_APACHE2 b/clatd/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/clatd/MODULE_LICENSE_APACHE2
diff --git a/clatd/NOTICE b/clatd/NOTICE
new file mode 100644
index 0000000..5943b54
--- /dev/null
+++ b/clatd/NOTICE
@@ -0,0 +1,189 @@
+   Copyright (c) 2010-2012, Daniel Drown
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+
+   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.
+
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
diff --git a/clatd/PREUPLOAD.cfg b/clatd/PREUPLOAD.cfg
new file mode 100644
index 0000000..c8dbf77
--- /dev/null
+++ b/clatd/PREUPLOAD.cfg
@@ -0,0 +1,5 @@
+[Builtin Hooks]
+clang_format = true
+
+[Builtin Hooks Options]
+clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp
diff --git a/clatd/TEST_MAPPING b/clatd/TEST_MAPPING
new file mode 100644
index 0000000..d36908a
--- /dev/null
+++ b/clatd/TEST_MAPPING
@@ -0,0 +1,10 @@
+{
+  "presubmit": [
+    { "name": "clatd_test" },
+    { "name": "netd_integration_test" },
+    { "name": "netd_unit_test" },
+    { "name": "netdutils_test" },
+    { "name": "resolv_integration_test" },
+    { "name": "resolv_unit_test" }
+  ]
+}
diff --git a/clatd/clatd.c b/clatd/clatd.c
new file mode 100644
index 0000000..bac8b1d
--- /dev/null
+++ b/clatd/clatd.c
@@ -0,0 +1,307 @@
+/*
+ * Copyright 2012 Daniel Drown
+ *
+ * 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.
+ *
+ * clatd.c - tun interface setup and main event loop
+ */
+#include <arpa/inet.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/prctl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <linux/filter.h>
+#include <linux/if.h>
+#include <linux/if_ether.h>
+#include <linux/if_packet.h>
+#include <linux/if_tun.h>
+#include <linux/virtio_net.h>
+#include <net/if.h>
+#include <sys/uio.h>
+
+#include "clatd.h"
+#include "checksum.h"
+#include "config.h"
+#include "dump.h"
+#include "logging.h"
+#include "translate.h"
+
+struct clat_config Global_Clatd_Config;
+
+volatile sig_atomic_t running = 1;
+
+// reads IPv6 packet from AF_PACKET socket, translates to IPv4, writes to tun
+void process_packet_6_to_4(struct tun_data *tunnel) {
+  // ethernet header is 14 bytes, plus 4 for a normal VLAN tag or 8 for Q-in-Q
+  // we don't really support vlans (or especially Q-in-Q)...
+  // but a few bytes of extra buffer space doesn't hurt...
+  struct {
+    struct virtio_net_hdr vnet;
+    uint8_t payload[22 + MAXMTU];
+    char pad; // +1 to make packet truncation obvious
+  } buf;
+  struct iovec iov = {
+    .iov_base = &buf,
+    .iov_len = sizeof(buf),
+  };
+  char cmsg_buf[CMSG_SPACE(sizeof(struct tpacket_auxdata))];
+  struct msghdr msgh = {
+    .msg_iov = &iov,
+    .msg_iovlen = 1,
+    .msg_control = cmsg_buf,
+    .msg_controllen = sizeof(cmsg_buf),
+  };
+  ssize_t readlen = recvmsg(tunnel->read_fd6, &msgh, /*flags*/ 0);
+
+  if (readlen < 0) {
+    if (errno != EAGAIN) {
+      logmsg(ANDROID_LOG_WARN, "%s: read error: %s", __func__, strerror(errno));
+    }
+    return;
+  } else if (readlen == 0) {
+    logmsg(ANDROID_LOG_WARN, "%s: packet socket removed?", __func__);
+    running = 0;
+    return;
+  } else if (readlen >= sizeof(buf)) {
+    logmsg(ANDROID_LOG_WARN, "%s: read truncation - ignoring pkt", __func__);
+    return;
+  }
+
+  bool ok = false;
+  __u32 tp_status = 0;
+  __u16 tp_net = 0;
+
+  for (struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msgh); cmsg != NULL; cmsg = CMSG_NXTHDR(&msgh,cmsg)) {
+    if (cmsg->cmsg_level == SOL_PACKET && cmsg->cmsg_type == PACKET_AUXDATA) {
+      struct tpacket_auxdata *aux = (struct tpacket_auxdata *)CMSG_DATA(cmsg);
+      ok = true;
+      tp_status = aux->tp_status;
+      tp_net = aux->tp_net;
+      break;
+    }
+  }
+
+  if (!ok) {
+    // theoretically this should not happen...
+    static bool logged = false;
+    if (!logged) {
+      logmsg(ANDROID_LOG_ERROR, "%s: failed to fetch tpacket_auxdata cmsg", __func__);
+      logged = true;
+    }
+  }
+
+  const int payload_offset = offsetof(typeof(buf), payload);
+  if (readlen < payload_offset + tp_net) {
+    logmsg(ANDROID_LOG_WARN, "%s: ignoring %zd byte pkt shorter than %d+%u L2 header",
+           __func__, readlen, payload_offset, tp_net);
+    return;
+  }
+
+  const int pkt_len = readlen - payload_offset;
+
+  // This will detect a skb->ip_summed == CHECKSUM_PARTIAL packet with non-final L4 checksum
+  if (tp_status & TP_STATUS_CSUMNOTREADY) {
+    static bool logged = false;
+    if (!logged) {
+      logmsg(ANDROID_LOG_WARN, "%s: L4 checksum calculation required", __func__);
+      logged = true;
+    }
+
+    // These are non-negative by virtue of csum_start/offset being u16
+    const int cs_start = buf.vnet.csum_start;
+    const int cs_offset = cs_start + buf.vnet.csum_offset;
+    if (cs_start > pkt_len) {
+      logmsg(ANDROID_LOG_ERROR, "%s: out of range - checksum start %d > %d",
+             __func__, cs_start, pkt_len);
+    } else if (cs_offset + 1 >= pkt_len) {
+      logmsg(ANDROID_LOG_ERROR, "%s: out of range - checksum offset %d + 1 >= %d",
+             __func__, cs_offset, pkt_len);
+    } else {
+      uint16_t csum = ip_checksum(buf.payload + cs_start, pkt_len - cs_start);
+      if (!csum) csum = 0xFFFF;  // required fixup for UDP, TCP must live with it
+      buf.payload[cs_offset] = csum & 0xFF;
+      buf.payload[cs_offset + 1] = csum >> 8;
+    }
+  }
+
+  translate_packet(tunnel->fd4, 0 /* to_ipv6 */, buf.payload + tp_net, pkt_len - tp_net);
+}
+
+// reads TUN_PI + L3 IPv4 packet from tun, translates to IPv6, writes to AF_INET6/RAW socket
+void process_packet_4_to_6(struct tun_data *tunnel) {
+  struct {
+    struct tun_pi pi;
+    uint8_t payload[MAXMTU];
+    char pad; // +1 byte to make packet truncation obvious
+  } buf;
+  ssize_t readlen = read(tunnel->fd4, &buf, sizeof(buf));
+
+  if (readlen < 0) {
+    if (errno != EAGAIN) {
+      logmsg(ANDROID_LOG_WARN, "%s: read error: %s", __func__, strerror(errno));
+    }
+    return;
+  } else if (readlen == 0) {
+    logmsg(ANDROID_LOG_WARN, "%s: tun interface removed", __func__);
+    running = 0;
+    return;
+  } else if (readlen >= sizeof(buf)) {
+    logmsg(ANDROID_LOG_WARN, "%s: read truncation - ignoring pkt", __func__);
+    return;
+  }
+
+  const int payload_offset = offsetof(typeof(buf), payload);
+
+  if (readlen < payload_offset) {
+    logmsg(ANDROID_LOG_WARN, "%s: short read: got %ld bytes", __func__, readlen);
+    return;
+  }
+
+  const int pkt_len = readlen - payload_offset;
+
+  uint16_t proto = ntohs(buf.pi.proto);
+  if (proto != ETH_P_IP) {
+    logmsg(ANDROID_LOG_WARN, "%s: unknown packet type = 0x%x", __func__, proto);
+    return;
+  }
+
+  if (buf.pi.flags != 0) {
+    logmsg(ANDROID_LOG_WARN, "%s: unexpected flags = %d", __func__, buf.pi.flags);
+  }
+
+  translate_packet(tunnel->write_fd6, 1 /* to_ipv6 */, buf.payload, pkt_len);
+}
+
+// IPv6 DAD packet format:
+//   Ethernet header (if needed) will be added by the kernel:
+//     u8[6] src_mac; u8[6] dst_mac '33:33:ff:XX:XX:XX'; be16 ethertype '0x86DD'
+//   IPv6 header:
+//     be32 0x60000000 - ipv6, tclass 0, flowlabel 0
+//     be16 payload_length '32'; u8 nxt_hdr ICMPv6 '58'; u8 hop limit '255'
+//     u128 src_ip6 '::'
+//     u128 dst_ip6 'ff02::1:ffXX:XXXX'
+//   ICMPv6 header:
+//     u8 type '135'; u8 code '0'; u16 icmp6 checksum; u32 reserved '0'
+//   ICMPv6 neighbour solicitation payload:
+//     u128 tgt_ip6
+//   ICMPv6 ND options:
+//     u8 opt nr '14'; u8 length '1'; u8[6] nonce '6 random bytes'
+void send_dad(int fd, const struct in6_addr* tgt) {
+  struct {
+    struct ip6_hdr ip6h;
+    struct nd_neighbor_solicit ns;
+    uint8_t ns_opt_nr;
+    uint8_t ns_opt_len;
+    uint8_t ns_opt_nonce[6];
+  } dad_pkt = {
+    .ip6h = {
+      .ip6_flow = htonl(6 << 28),  // v6, 0 tclass, 0 flowlabel
+      .ip6_plen = htons(sizeof(dad_pkt) - sizeof(struct ip6_hdr)),  // payload length, ie. 32
+      .ip6_nxt = IPPROTO_ICMPV6,  // 58
+      .ip6_hlim = 255,
+      .ip6_src = {},  // ::
+      .ip6_dst.s6_addr = {
+        0xFF, 0x02, 0, 0,
+        0, 0, 0, 0,
+        0, 0, 0, 1,
+        0xFF, tgt->s6_addr[13], tgt->s6_addr[14], tgt->s6_addr[15],
+      },  // ff02::1:ffXX:XXXX - multicast group address derived from bottom 24-bits of tgt
+    },
+    .ns = {
+      .nd_ns_type = ND_NEIGHBOR_SOLICIT,  // 135
+      .nd_ns_code = 0,
+      .nd_ns_cksum = 0,  // will be calculated later
+      .nd_ns_reserved = 0,
+      .nd_ns_target = *tgt,
+    },
+    .ns_opt_nr = 14,  // icmp6 option 'nonce' from RFC3971
+    .ns_opt_len = 1,  // in units of 8 bytes, including option nr and len
+    .ns_opt_nonce = {},  // opt_len *8 - sizeof u8(opt_nr) - sizeof u8(opt_len) = 6 ranodmized bytes
+  };
+  arc4random_buf(&dad_pkt.ns_opt_nonce, sizeof(dad_pkt.ns_opt_nonce));
+
+  // 40 byte IPv6 header + 8 byte ICMPv6 header + 16 byte ipv6 target address + 8 byte nonce option
+  _Static_assert(sizeof(dad_pkt) == 40 + 8 + 16 + 8, "sizeof dad packet != 72");
+
+  // IPv6 header checksum is standard negated 16-bit one's complement sum over the icmpv6 pseudo
+  // header (which includes payload length, nextheader, and src/dst ip) and the icmpv6 payload.
+  //
+  // Src/dst ip immediately prefix the icmpv6 header itself, so can be handled along
+  // with the payload.  We thus only need to manually account for payload len & next header.
+  //
+  // The magic '8' is simply the offset of the ip6_src field in the ipv6 header,
+  // ie. we're skipping over the ipv6 version, tclass, flowlabel, payload length, next header
+  // and hop limit fields, because they're not quite where we want them to be.
+  //
+  // ip6_plen is already in network order, while ip6_nxt is a single byte and thus needs htons().
+  uint32_t csum = dad_pkt.ip6h.ip6_plen + htons(dad_pkt.ip6h.ip6_nxt);
+  csum = ip_checksum_add(csum, &dad_pkt.ip6h.ip6_src, sizeof(dad_pkt) - 8);
+  dad_pkt.ns.nd_ns_cksum = ip_checksum_finish(csum);
+
+  const struct sockaddr_in6 dst = {
+    .sin6_family = AF_INET6,
+    .sin6_addr = dad_pkt.ip6h.ip6_dst,
+    .sin6_scope_id = if_nametoindex(Global_Clatd_Config.native_ipv6_interface),
+  };
+
+  sendto(fd, &dad_pkt, sizeof(dad_pkt), 0 /*flags*/, (const struct sockaddr *)&dst, sizeof(dst));
+}
+
+/* function: event_loop
+ * reads packets from the tun network interface and passes them down the stack
+ *   tunnel - tun device data
+ */
+void event_loop(struct tun_data *tunnel) {
+  // Apparently some network gear will refuse to perform NS for IPs that aren't DAD'ed,
+  // this would then result in an ipv6-only network with working native ipv6, working
+  // IPv4 via DNS64, but non-functioning IPv4 via CLAT (ie. IPv4 literals + IPv4 only apps).
+  // The kernel itself doesn't do DAD for anycast ips (but does handle IPV6 MLD and handle ND).
+  // So we'll spoof dad here, and yeah, we really should check for a response and in
+  // case of failure pick a different IP.  Seeing as 48-bits of the IP are utterly random
+  // (with the other 16 chosen to guarantee checksum neutrality) this seems like a remote
+  // concern...
+  // TODO: actually perform true DAD
+  send_dad(tunnel->write_fd6, &Global_Clatd_Config.ipv6_local_subnet);
+
+  struct pollfd wait_fd[] = {
+    { tunnel->read_fd6, POLLIN, 0 },
+    { tunnel->fd4, POLLIN, 0 },
+  };
+
+  while (running) {
+    if (poll(wait_fd, ARRAY_SIZE(wait_fd), -1) == -1) {
+      if (errno != EINTR) {
+        logmsg(ANDROID_LOG_WARN, "event_loop/poll returned an error: %s", strerror(errno));
+      }
+    } else {
+      // Call process_packet if the socket has data to be read, but also if an
+      // error is waiting. If we don't call read() after getting POLLERR, a
+      // subsequent poll() will return immediately with POLLERR again,
+      // causing this code to spin in a loop. Calling read() will clear the
+      // socket error flag instead.
+      if (wait_fd[0].revents) process_packet_6_to_4(tunnel);
+      if (wait_fd[1].revents) process_packet_4_to_6(tunnel);
+    }
+  }
+}
diff --git a/clatd/clatd.h b/clatd/clatd.h
new file mode 100644
index 0000000..e170c58
--- /dev/null
+++ b/clatd/clatd.h
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2011 Daniel Drown
+ *
+ * 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.
+ *
+ * clatd.h - main routines used by clatd
+ */
+#ifndef __CLATD_H__
+#define __CLATD_H__
+
+#include <signal.h>
+#include <stdlib.h>
+#include <sys/uio.h>
+
+struct tun_data;
+
+// IPv4 header has a u16 total length field, for maximum L3 mtu of 0xFFFF.
+//
+// Translating IPv4 to IPv6 requires removing the IPv4 header (20) and adding
+// an IPv6 header (40), possibly with an extra ipv6 fragment extension header (8).
+//
+// As such the maximum IPv4 L3 mtu size is 0xFFFF (by u16 tot_len field)
+// and the maximum IPv6 L3 mtu size is 0xFFFF + 28 (which is larger)
+//
+// A received non-jumbogram IPv6 frame could potentially be u16 payload_len = 0xFFFF
+// + sizeof ipv6 header = 40, bytes in size.  But such a packet cannot be meaningfully
+// converted to IPv4 (it's too large).  As such the restriction is the same: 0xFFFF + 28
+//
+// (since there's no jumbogram support in IPv4, IPv6 jumbograms cannot be meaningfully
+// converted to IPv4 anyway, and are thus entirely unsupported)
+#define MAXMTU (0xFFFF + 28)
+
+// logcat_hexdump() maximum binary data length, this is the maximum packet size
+// plus some extra space for various headers:
+//   struct tun_pi (4 bytes)
+//   struct virtio_net_hdr (10 bytes)
+//   ethernet (14 bytes), potentially including vlan tag (4) or tags (8 or 12)
+// plus some extra just-in-case headroom, because it doesn't hurt.
+#define MAXDUMPLEN (64 + MAXMTU)
+
+#define CLATD_VERSION "1.7"
+
+#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
+
+extern volatile sig_atomic_t running;
+
+void event_loop(struct tun_data *tunnel);
+
+/* function: parse_int
+ * parses a string as a decimal/hex/octal signed integer
+ *   str - the string to parse
+ *   out - the signed integer to write to, gets clobbered on failure
+ */
+static inline int parse_int(const char *str, int *out) {
+  char *end_ptr;
+  *out = strtol(str, &end_ptr, 0);
+  return *str && !*end_ptr;
+}
+
+/* function: parse_unsigned
+ * parses a string as a decimal/hex/octal unsigned integer
+ *   str - the string to parse
+ *   out - the unsigned integer to write to, gets clobbered on failure
+ */
+static inline int parse_unsigned(const char *str, unsigned *out) {
+  char *end_ptr;
+  *out = strtoul(str, &end_ptr, 0);
+  return *str && !*end_ptr;
+}
+
+#endif /* __CLATD_H__ */
diff --git a/clatd/clatd_test.cpp b/clatd/clatd_test.cpp
new file mode 100644
index 0000000..0ed5f28
--- /dev/null
+++ b/clatd/clatd_test.cpp
@@ -0,0 +1,835 @@
+/*
+ * Copyright 2014 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.
+ *
+ * clatd_test.cpp - unit tests for clatd
+ */
+
+#include <iostream>
+
+#include <arpa/inet.h>
+#include <linux/if_packet.h>
+#include <netinet/in6.h>
+#include <stdio.h>
+#include <sys/uio.h>
+
+#include <gtest/gtest.h>
+
+#include "netutils/ifc.h"
+#include "tun_interface.h"
+
+extern "C" {
+#include "checksum.h"
+#include "clatd.h"
+#include "config.h"
+#include "translate.h"
+}
+
+// For convenience.
+#define ARRAYSIZE(x) sizeof((x)) / sizeof((x)[0])
+
+using android::net::TunInterface;
+
+// Default translation parameters.
+static const char kIPv4LocalAddr[]  = "192.0.0.4";
+static const char kIPv6LocalAddr[]  = "2001:db8:0:b11::464";
+static const char kIPv6PlatSubnet[] = "64:ff9b::";
+
+// clang-format off
+// Test packet portions. Defined as macros because it's easy to concatenate them to make packets.
+#define IPV4_HEADER(p, c1, c2) \
+    0x45, 0x00,    0,   41,  /* Version=4, IHL=5, ToS=0x80, len=41 */     \
+    0x00, 0x00, 0x40, 0x00,  /* ID=0x0000, flags=IP_DF, offset=0 */       \
+      55,  (p), (c1), (c2),  /* TTL=55, protocol=p, checksum=c1,c2 */     \
+     192,    0,    0,    4,  /* Src=192.0.0.4 */                          \
+       8,    8,    8,    8,  /* Dst=8.8.8.8 */
+#define IPV4_UDP_HEADER IPV4_HEADER(IPPROTO_UDP, 0x73, 0xb0)
+#define IPV4_ICMP_HEADER IPV4_HEADER(IPPROTO_ICMP, 0x73, 0xc0)
+
+#define IPV6_HEADER(p) \
+    0x60, 0x00,    0,    0,  /* Version=6, tclass=0x00, flowlabel=0 */    \
+       0,   21,  (p),   55,  /* plen=11, nxthdr=p, hlim=55 */             \
+    0x20, 0x01, 0x0d, 0xb8,  /* Src=2001:db8:0:b11::464 */                \
+    0x00, 0x00, 0x0b, 0x11,                                               \
+    0x00, 0x00, 0x00, 0x00,                                               \
+    0x00, 0x00, 0x04, 0x64,                                               \
+    0x00, 0x64, 0xff, 0x9b,  /* Dst=64:ff9b::8.8.8.8 */                   \
+    0x00, 0x00, 0x00, 0x00,                                               \
+    0x00, 0x00, 0x00, 0x00,                                               \
+    0x08, 0x08, 0x08, 0x08,
+#define IPV6_UDP_HEADER IPV6_HEADER(IPPROTO_UDP)
+#define IPV6_ICMPV6_HEADER IPV6_HEADER(IPPROTO_ICMPV6)
+
+#define UDP_LEN 21
+#define UDP_HEADER \
+    0xc8, 0x8b,    0,   53,  /* Port 51339->53 */                         \
+    0x00, UDP_LEN, 0,    0,  /* Length 21, checksum empty for now */
+
+#define PAYLOAD 'H', 'e', 'l', 'l', 'o', ' ', 0x4e, 0xb8, 0x96, 0xe7, 0x95, 0x8c, 0x00
+
+#define IPV4_PING \
+    0x08, 0x00, 0x88, 0xd0,  /* Type 8, code 0, checksum 0x88d0 */        \
+    0xd0, 0x0d, 0x00, 0x03,  /* ID=0xd00d, seq=3 */
+
+#define IPV6_PING \
+    0x80, 0x00, 0xc3, 0x42,  /* Type 128, code 0, checksum 0xc342 */      \
+    0xd0, 0x0d, 0x00, 0x03,  /* ID=0xd00d, seq=3 */
+
+// Macros to return pseudo-headers from packets.
+#define IPV4_PSEUDOHEADER(ip, tlen)                                  \
+  ip[12], ip[13], ip[14], ip[15],        /* Source address      */   \
+  ip[16], ip[17], ip[18], ip[19],        /* Destination address */   \
+  0, ip[9],                              /* 0, protocol         */   \
+  ((tlen) >> 16) & 0xff, (tlen) & 0xff,  /* Transport length */
+
+#define IPV6_PSEUDOHEADER(ip6, protocol, tlen)                       \
+  ip6[8],  ip6[9],  ip6[10], ip6[11],  /* Source address */          \
+  ip6[12], ip6[13], ip6[14], ip6[15],                                \
+  ip6[16], ip6[17], ip6[18], ip6[19],                                \
+  ip6[20], ip6[21], ip6[22], ip6[23],                                \
+  ip6[24], ip6[25], ip6[26], ip6[27],  /* Destination address */     \
+  ip6[28], ip6[29], ip6[30], ip6[31],                                \
+  ip6[32], ip6[33], ip6[34], ip6[35],                                \
+  ip6[36], ip6[37], ip6[38], ip6[39],                                \
+  ((tlen) >> 24) & 0xff,               /* Transport length */        \
+  ((tlen) >> 16) & 0xff,                                             \
+  ((tlen) >> 8) & 0xff,                                              \
+  (tlen) & 0xff,                                                     \
+  0, 0, 0, (protocol),
+
+// A fragmented DNS request.
+static const uint8_t kIPv4Frag1[] = {
+    0x45, 0x00, 0x00, 0x24, 0xfe, 0x47, 0x20, 0x00, 0x40, 0x11,
+    0x8c, 0x6d, 0xc0, 0x00, 0x00, 0x04, 0x08, 0x08, 0x08, 0x08,
+    0x14, 0x5d, 0x00, 0x35, 0x00, 0x29, 0x68, 0xbb, 0x50, 0x47,
+    0x01, 0x00, 0x00, 0x01, 0x00, 0x00
+};
+static const uint8_t kIPv4Frag2[] = {
+    0x45, 0x00, 0x00, 0x24, 0xfe, 0x47, 0x20, 0x02, 0x40, 0x11,
+    0x8c, 0x6b, 0xc0, 0x00, 0x00, 0x04, 0x08, 0x08, 0x08, 0x08,
+    0x00, 0x00, 0x00, 0x00, 0x04, 0x69, 0x70, 0x76, 0x34, 0x06,
+    0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65
+};
+static const uint8_t kIPv4Frag3[] = {
+    0x45, 0x00, 0x00, 0x1d, 0xfe, 0x47, 0x00, 0x04, 0x40, 0x11,
+    0xac, 0x70, 0xc0, 0x00, 0x00, 0x04, 0x08, 0x08, 0x08, 0x08,
+    0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, 0x01
+};
+static const uint8_t *kIPv4Fragments[] = { kIPv4Frag1, kIPv4Frag2, kIPv4Frag3 };
+static const size_t kIPv4FragLengths[] = { sizeof(kIPv4Frag1), sizeof(kIPv4Frag2),
+                                           sizeof(kIPv4Frag3) };
+
+static const uint8_t kIPv6Frag1[] = {
+    0x60, 0x00, 0x00, 0x00, 0x00, 0x18, 0x2c, 0x40, 0x20, 0x01,
+    0x0d, 0xb8, 0x00, 0x00, 0x0b, 0x11, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x04, 0x64, 0x00, 0x64, 0xff, 0x9b, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x08, 0x08, 0x08,
+    0x11, 0x00, 0x00, 0x01, 0x00, 0x00, 0xfe, 0x47, 0x14, 0x5d,
+    0x00, 0x35, 0x00, 0x29, 0xeb, 0x91, 0x50, 0x47, 0x01, 0x00,
+    0x00, 0x01, 0x00, 0x00
+};
+
+static const uint8_t kIPv6Frag2[] = {
+    0x60, 0x00, 0x00, 0x00, 0x00, 0x18, 0x2c, 0x40, 0x20, 0x01,
+    0x0d, 0xb8, 0x00, 0x00, 0x0b, 0x11, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x04, 0x64, 0x00, 0x64, 0xff, 0x9b, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x08, 0x08, 0x08,
+    0x11, 0x00, 0x00, 0x11, 0x00, 0x00, 0xfe, 0x47, 0x00, 0x00,
+    0x00, 0x00, 0x04, 0x69, 0x70, 0x76, 0x34, 0x06, 0x67, 0x6f,
+    0x6f, 0x67, 0x6c, 0x65
+};
+
+static const uint8_t kIPv6Frag3[] = {
+    0x60, 0x00, 0x00, 0x00, 0x00, 0x11, 0x2c, 0x40, 0x20, 0x01,
+    0x0d, 0xb8, 0x00, 0x00, 0x0b, 0x11, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x04, 0x64, 0x00, 0x64, 0xff, 0x9b, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x08, 0x08, 0x08,
+    0x11, 0x00, 0x00, 0x20, 0x00, 0x00, 0xfe, 0x47, 0x03, 0x63,
+    0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, 0x01
+};
+static const uint8_t *kIPv6Fragments[] = { kIPv6Frag1, kIPv6Frag2, kIPv6Frag3 };
+static const size_t kIPv6FragLengths[] = { sizeof(kIPv6Frag1), sizeof(kIPv6Frag2),
+                                           sizeof(kIPv6Frag3) };
+
+static const uint8_t kReassembledIPv4[] = {
+    0x45, 0x00, 0x00, 0x3d, 0xfe, 0x47, 0x00, 0x00, 0x40, 0x11,
+    0xac, 0x54, 0xc0, 0x00, 0x00, 0x04, 0x08, 0x08, 0x08, 0x08,
+    0x14, 0x5d, 0x00, 0x35, 0x00, 0x29, 0x68, 0xbb, 0x50, 0x47,
+    0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x04, 0x69, 0x70, 0x76, 0x34, 0x06, 0x67, 0x6f, 0x6f, 0x67,
+    0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00,
+    0x01
+};
+// clang-format on
+
+// Expected checksums.
+static const uint32_t kUdpPartialChecksum     = 0xd5c8;
+static const uint32_t kPayloadPartialChecksum = 0x31e9c;
+static const uint16_t kUdpV4Checksum          = 0xd0c7;
+static const uint16_t kUdpV6Checksum          = 0xa74a;
+
+uint8_t ip_version(const uint8_t *packet) {
+  uint8_t version = packet[0] >> 4;
+  return version;
+}
+
+int is_ipv4_fragment(struct iphdr *ip) {
+  // A packet is a fragment if its fragment offset is nonzero or if the MF flag is set.
+  return ntohs(ip->frag_off) & (IP_OFFMASK | IP_MF);
+}
+
+int is_ipv6_fragment(struct ip6_hdr *ip6, size_t len) {
+  if (ip6->ip6_nxt != IPPROTO_FRAGMENT) {
+    return 0;
+  }
+  struct ip6_frag *frag = (struct ip6_frag *)(ip6 + 1);
+  return len >= sizeof(*ip6) + sizeof(*frag) &&
+         (frag->ip6f_offlg & (IP6F_OFF_MASK | IP6F_MORE_FRAG));
+}
+
+int ipv4_fragment_offset(struct iphdr *ip) {
+  return ntohs(ip->frag_off) & IP_OFFMASK;
+}
+
+int ipv6_fragment_offset(struct ip6_frag *frag) {
+  return ntohs((frag->ip6f_offlg & IP6F_OFF_MASK) >> 3);
+}
+
+void check_packet(const uint8_t *packet, size_t len, const char *msg) {
+  void *payload;
+  size_t payload_length    = 0;
+  uint32_t pseudo_checksum = 0;
+  uint8_t protocol         = 0;
+  int version              = ip_version(packet);
+  switch (version) {
+    case 4: {
+      struct iphdr *ip = (struct iphdr *)packet;
+      ASSERT_GE(len, sizeof(*ip)) << msg << ": IPv4 packet shorter than IPv4 header\n";
+      EXPECT_EQ(5, ip->ihl) << msg << ": Unsupported IP header length\n";
+      EXPECT_EQ(len, ntohs(ip->tot_len)) << msg << ": Incorrect IPv4 length\n";
+      EXPECT_EQ(0, ip_checksum(ip, sizeof(*ip))) << msg << ": Incorrect IP checksum\n";
+      protocol = ip->protocol;
+      payload  = ip + 1;
+      if (!is_ipv4_fragment(ip)) {
+        payload_length  = len - sizeof(*ip);
+        pseudo_checksum = ipv4_pseudo_header_checksum(ip, payload_length);
+      }
+      ASSERT_TRUE(protocol == IPPROTO_TCP || protocol == IPPROTO_UDP || protocol == IPPROTO_ICMP)
+        << msg << ": Unsupported IPv4 protocol " << protocol << "\n";
+      break;
+    }
+    case 6: {
+      struct ip6_hdr *ip6 = (struct ip6_hdr *)packet;
+      ASSERT_GE(len, sizeof(*ip6)) << msg << ": IPv6 packet shorter than IPv6 header\n";
+      EXPECT_EQ(len - sizeof(*ip6), htons(ip6->ip6_plen)) << msg << ": Incorrect IPv6 length\n";
+
+      if (ip6->ip6_nxt == IPPROTO_FRAGMENT) {
+        struct ip6_frag *frag = (struct ip6_frag *)(ip6 + 1);
+        ASSERT_GE(len, sizeof(*ip6) + sizeof(*frag))
+          << msg << ": IPv6 fragment: short fragment header\n";
+        protocol = frag->ip6f_nxt;
+        payload  = frag + 1;
+        // Even though the packet has a Fragment header, it might not be a fragment.
+        if (!is_ipv6_fragment(ip6, len)) {
+          payload_length = len - sizeof(*ip6) - sizeof(*frag);
+        }
+      } else {
+        // Since there are no extension headers except Fragment, this must be the payload.
+        protocol       = ip6->ip6_nxt;
+        payload        = ip6 + 1;
+        payload_length = len - sizeof(*ip6);
+      }
+      ASSERT_TRUE(protocol == IPPROTO_TCP || protocol == IPPROTO_UDP || protocol == IPPROTO_ICMPV6)
+        << msg << ": Unsupported IPv6 next header " << protocol;
+      if (payload_length) {
+        pseudo_checksum = ipv6_pseudo_header_checksum(ip6, payload_length, protocol);
+      }
+      break;
+    }
+    default:
+      FAIL() << msg << ": Unsupported IP version " << version << "\n";
+      return;
+  }
+
+  // If we understand the payload, verify the checksum.
+  if (payload_length) {
+    uint16_t checksum;
+    switch (protocol) {
+      case IPPROTO_UDP:
+      case IPPROTO_TCP:
+      case IPPROTO_ICMPV6:
+        checksum = ip_checksum_finish(ip_checksum_add(pseudo_checksum, payload, payload_length));
+        break;
+      case IPPROTO_ICMP:
+        checksum = ip_checksum(payload, payload_length);
+        break;
+      default:
+        checksum = 0;  // Don't check.
+        break;
+    }
+    EXPECT_EQ(0, checksum) << msg << ": Incorrect transport checksum\n";
+  }
+
+  if (protocol == IPPROTO_UDP) {
+    struct udphdr *udp = (struct udphdr *)payload;
+    EXPECT_NE(0, udp->check) << msg << ": UDP checksum 0 should be 0xffff";
+    // If this is not a fragment, check the UDP length field.
+    if (payload_length) {
+      EXPECT_EQ(payload_length, ntohs(udp->len)) << msg << ": Incorrect UDP length\n";
+    }
+  }
+}
+
+void reassemble_packet(const uint8_t **fragments, const size_t lengths[], int numpackets,
+                       uint8_t *reassembled, size_t *reassembled_len, const char *msg) {
+  struct iphdr *ip    = nullptr;
+  struct ip6_hdr *ip6 = nullptr;
+  size_t total_length, pos = 0;
+  uint8_t protocol = 0;
+  uint8_t version  = ip_version(fragments[0]);
+
+  for (int i = 0; i < numpackets; i++) {
+    const uint8_t *packet = fragments[i];
+    int len               = lengths[i];
+    int headersize, payload_offset;
+
+    ASSERT_EQ(ip_version(packet), version) << msg << ": Inconsistent fragment versions\n";
+    check_packet(packet, len, "Fragment sanity check");
+
+    switch (version) {
+      case 4: {
+        struct iphdr *ip_orig = (struct iphdr *)packet;
+        headersize            = sizeof(*ip_orig);
+        ASSERT_TRUE(is_ipv4_fragment(ip_orig))
+          << msg << ": IPv4 fragment #" << i + 1 << " not a fragment\n";
+        ASSERT_EQ(pos, ipv4_fragment_offset(ip_orig) * 8 + ((i != 0) ? sizeof(*ip) : 0))
+          << msg << ": IPv4 fragment #" << i + 1 << ": inconsistent offset\n";
+
+        headersize     = sizeof(*ip_orig);
+        payload_offset = headersize;
+        if (pos == 0) {
+          ip = (struct iphdr *)reassembled;
+        }
+        break;
+      }
+      case 6: {
+        struct ip6_hdr *ip6_orig = (struct ip6_hdr *)packet;
+        struct ip6_frag *frag    = (struct ip6_frag *)(ip6_orig + 1);
+        ASSERT_TRUE(is_ipv6_fragment(ip6_orig, len))
+          << msg << ": IPv6 fragment #" << i + 1 << " not a fragment\n";
+        ASSERT_EQ(pos, ipv6_fragment_offset(frag) * 8 + ((i != 0) ? sizeof(*ip6) : 0))
+          << msg << ": IPv6 fragment #" << i + 1 << ": inconsistent offset\n";
+
+        headersize     = sizeof(*ip6_orig);
+        payload_offset = sizeof(*ip6_orig) + sizeof(*frag);
+        if (pos == 0) {
+          ip6      = (struct ip6_hdr *)reassembled;
+          protocol = frag->ip6f_nxt;
+        }
+        break;
+      }
+      default:
+        FAIL() << msg << ": Invalid IP version << " << version;
+    }
+
+    // If this is the first fragment, copy the header.
+    if (pos == 0) {
+      ASSERT_LT(headersize, (int)*reassembled_len) << msg << ": Reassembly buffer too small\n";
+      memcpy(reassembled, packet, headersize);
+      total_length = headersize;
+      pos += headersize;
+    }
+
+    // Copy the payload.
+    int payload_length = len - payload_offset;
+    total_length += payload_length;
+    ASSERT_LT(total_length, *reassembled_len) << msg << ": Reassembly buffer too small\n";
+    memcpy(reassembled + pos, packet + payload_offset, payload_length);
+    pos += payload_length;
+  }
+
+  // Fix up the reassembled headers to reflect fragmentation and length (and IPv4 checksum).
+  ASSERT_EQ(total_length, pos) << msg << ": Reassembled packet length incorrect\n";
+  if (ip) {
+    ip->frag_off &= ~htons(IP_MF);
+    ip->tot_len = htons(total_length);
+    ip->check   = 0;
+    ip->check   = ip_checksum(ip, sizeof(*ip));
+    ASSERT_FALSE(is_ipv4_fragment(ip)) << msg << ": reassembled IPv4 packet is a fragment!\n";
+  }
+  if (ip6) {
+    ip6->ip6_nxt  = protocol;
+    ip6->ip6_plen = htons(total_length - sizeof(*ip6));
+    ASSERT_FALSE(is_ipv6_fragment(ip6, ip6->ip6_plen))
+      << msg << ": reassembled IPv6 packet is a fragment!\n";
+  }
+
+  *reassembled_len = total_length;
+}
+
+void check_data_matches(const void *expected, const void *actual, size_t len, const char *msg) {
+  if (memcmp(expected, actual, len)) {
+    // Hex dump, 20 bytes per line, one space between bytes (1 byte = 3 chars), indented by 4.
+    int hexdump_len = len * 3 + (len / 20 + 1) * 5;
+    char expected_hexdump[hexdump_len], actual_hexdump[hexdump_len];
+    unsigned pos = 0;
+    for (unsigned i = 0; i < len; i++) {
+      if (i % 20 == 0) {
+        snprintf(expected_hexdump + pos, hexdump_len - pos, "\n   ");
+        snprintf(actual_hexdump + pos, hexdump_len - pos, "\n   ");
+        pos += 4;
+      }
+      snprintf(expected_hexdump + pos, hexdump_len - pos, " %02x", ((uint8_t *)expected)[i]);
+      snprintf(actual_hexdump + pos, hexdump_len - pos, " %02x", ((uint8_t *)actual)[i]);
+      pos += 3;
+    }
+    FAIL() << msg << ": Data doesn't match"
+           << "\n  Expected:" << (char *) expected_hexdump
+           << "\n  Actual:" << (char *) actual_hexdump << "\n";
+  }
+}
+
+void fix_udp_checksum(uint8_t *packet) {
+  uint32_t pseudo_checksum;
+  uint8_t version = ip_version(packet);
+  struct udphdr *udp;
+  switch (version) {
+    case 4: {
+      struct iphdr *ip = (struct iphdr *)packet;
+      udp              = (struct udphdr *)(ip + 1);
+      pseudo_checksum  = ipv4_pseudo_header_checksum(ip, ntohs(udp->len));
+      break;
+    }
+    case 6: {
+      struct ip6_hdr *ip6 = (struct ip6_hdr *)packet;
+      udp                 = (struct udphdr *)(ip6 + 1);
+      pseudo_checksum     = ipv6_pseudo_header_checksum(ip6, ntohs(udp->len), IPPROTO_UDP);
+      break;
+    }
+    default:
+      FAIL() << "unsupported IP version" << version << "\n";
+      return;
+  }
+
+  udp->check = 0;
+  udp->check = ip_checksum_finish(ip_checksum_add(pseudo_checksum, udp, ntohs(udp->len)));
+}
+
+// Testing stub for send_rawv6. The real version uses sendmsg() with a
+// destination IPv6 address, and attempting to call that on our test socketpair
+// fd results in EINVAL.
+extern "C" void send_rawv6(int fd, clat_packet out, int iov_len) { writev(fd, out, iov_len); }
+
+void do_translate_packet(const uint8_t *original, size_t original_len, uint8_t *out, size_t *outlen,
+                         const char *msg) {
+  int fds[2];
+  if (socketpair(AF_UNIX, SOCK_DGRAM | SOCK_NONBLOCK, 0, fds)) {
+    abort();
+  }
+
+  char foo[512];
+  snprintf(foo, sizeof(foo), "%s: Invalid original packet", msg);
+  check_packet(original, original_len, foo);
+
+  int read_fd, write_fd;
+  uint16_t expected_proto;
+  int version = ip_version(original);
+  switch (version) {
+    case 4:
+      expected_proto = htons(ETH_P_IPV6);
+      read_fd        = fds[1];
+      write_fd       = fds[0];
+      break;
+    case 6:
+      expected_proto = htons(ETH_P_IP);
+      read_fd        = fds[0];
+      write_fd       = fds[1];
+      break;
+    default:
+      FAIL() << msg << ": Unsupported IP version " << version << "\n";
+      break;
+  }
+
+  translate_packet(write_fd, (version == 4), original, original_len);
+
+  snprintf(foo, sizeof(foo), "%s: Invalid translated packet", msg);
+  if (version == 6) {
+    // Translating to IPv4. Expect a tun header.
+    struct tun_pi new_tun_header;
+    struct iovec iov[] = {
+      { &new_tun_header, sizeof(new_tun_header) },
+      { out, *outlen },
+    };
+
+    int len = readv(read_fd, iov, 2);
+    if (len > (int)sizeof(new_tun_header)) {
+      ASSERT_LT((size_t)len, *outlen) << msg << ": Translated packet buffer too small\n";
+      EXPECT_EQ(expected_proto, new_tun_header.proto) << msg << "Unexpected tun proto\n";
+      *outlen = len - sizeof(new_tun_header);
+      check_packet(out, *outlen, msg);
+    } else {
+      FAIL() << msg << ": Packet was not translated: len=" << len;
+      *outlen = 0;
+    }
+  } else {
+    // Translating to IPv6. Expect raw packet.
+    *outlen = read(read_fd, out, *outlen);
+    check_packet(out, *outlen, msg);
+  }
+}
+
+void check_translated_packet(const uint8_t *original, size_t original_len, const uint8_t *expected,
+                             size_t expected_len, const char *msg) {
+  uint8_t translated[MAXMTU];
+  size_t translated_len = sizeof(translated);
+  do_translate_packet(original, original_len, translated, &translated_len, msg);
+  EXPECT_EQ(expected_len, translated_len) << msg << ": Translated packet length incorrect\n";
+  check_data_matches(expected, translated, translated_len, msg);
+}
+
+void check_fragment_translation(const uint8_t *original[], const size_t original_lengths[],
+                                const uint8_t *expected[], const size_t expected_lengths[],
+                                int numfragments, const char *msg) {
+  for (int i = 0; i < numfragments; i++) {
+    // Check that each of the fragments translates as expected.
+    char frag_msg[512];
+    snprintf(frag_msg, sizeof(frag_msg), "%s: fragment #%d", msg, i + 1);
+    check_translated_packet(original[i], original_lengths[i], expected[i], expected_lengths[i],
+                            frag_msg);
+  }
+
+  // Sanity check that reassembling the original and translated fragments produces valid packets.
+  uint8_t reassembled[MAXMTU];
+  size_t reassembled_len = sizeof(reassembled);
+  reassemble_packet(original, original_lengths, numfragments, reassembled, &reassembled_len, msg);
+  check_packet(reassembled, reassembled_len, msg);
+
+  uint8_t translated[MAXMTU];
+  size_t translated_len = sizeof(translated);
+  do_translate_packet(reassembled, reassembled_len, translated, &translated_len, msg);
+  check_packet(translated, translated_len, msg);
+}
+
+int get_transport_checksum(const uint8_t *packet) {
+  struct iphdr *ip;
+  struct ip6_hdr *ip6;
+  uint8_t protocol;
+  const void *payload;
+
+  int version = ip_version(packet);
+  switch (version) {
+    case 4:
+      ip = (struct iphdr *)packet;
+      if (is_ipv4_fragment(ip)) {
+        return -1;
+      }
+      protocol = ip->protocol;
+      payload  = ip + 1;
+      break;
+    case 6:
+      ip6      = (struct ip6_hdr *)packet;
+      protocol = ip6->ip6_nxt;
+      payload  = ip6 + 1;
+      break;
+    default:
+      return -1;
+  }
+
+  switch (protocol) {
+    case IPPROTO_UDP:
+      return ((struct udphdr *)payload)->check;
+
+    case IPPROTO_TCP:
+      return ((struct tcphdr *)payload)->check;
+
+    case IPPROTO_FRAGMENT:
+    default:
+      return -1;
+  }
+}
+
+class ClatdTest : public ::testing::Test {
+ protected:
+  static TunInterface sTun;
+
+  virtual void SetUp() {
+    inet_pton(AF_INET, kIPv4LocalAddr, &Global_Clatd_Config.ipv4_local_subnet);
+    inet_pton(AF_INET6, kIPv6PlatSubnet, &Global_Clatd_Config.plat_subnet);
+    memset(&Global_Clatd_Config.ipv6_local_subnet, 0, sizeof(in6_addr));
+    Global_Clatd_Config.native_ipv6_interface = const_cast<char *>(sTun.name().c_str());
+  }
+
+  // Static because setting up the tun interface takes about 40ms.
+  static void SetUpTestCase() { ASSERT_EQ(0, sTun.init()); }
+
+  // Closing the socket removes the interface and IP addresses.
+  static void TearDownTestCase() { sTun.destroy(); }
+};
+
+TunInterface ClatdTest::sTun;
+
+void expect_ipv6_addr_equal(struct in6_addr *expected, struct in6_addr *actual) {
+  if (!IN6_ARE_ADDR_EQUAL(expected, actual)) {
+    char expected_str[INET6_ADDRSTRLEN], actual_str[INET6_ADDRSTRLEN];
+    inet_ntop(AF_INET6, expected, expected_str, sizeof(expected_str));
+    inet_ntop(AF_INET6, actual, actual_str, sizeof(actual_str));
+    FAIL()
+        << "Unexpected IPv6 address:: "
+        << "\n  Expected: " << expected_str
+        << "\n  Actual:   " << actual_str
+        << "\n";
+  }
+}
+
+TEST_F(ClatdTest, TestIPv6PrefixEqual) {
+  EXPECT_TRUE(ipv6_prefix_equal(&Global_Clatd_Config.plat_subnet,
+                                &Global_Clatd_Config.plat_subnet));
+  EXPECT_FALSE(ipv6_prefix_equal(&Global_Clatd_Config.plat_subnet,
+                                 &Global_Clatd_Config.ipv6_local_subnet));
+
+  struct in6_addr subnet2 = Global_Clatd_Config.ipv6_local_subnet;
+  EXPECT_TRUE(ipv6_prefix_equal(&Global_Clatd_Config.ipv6_local_subnet, &subnet2));
+  EXPECT_TRUE(ipv6_prefix_equal(&subnet2, &Global_Clatd_Config.ipv6_local_subnet));
+
+  subnet2.s6_addr[6] = 0xff;
+  EXPECT_FALSE(ipv6_prefix_equal(&Global_Clatd_Config.ipv6_local_subnet, &subnet2));
+  EXPECT_FALSE(ipv6_prefix_equal(&subnet2, &Global_Clatd_Config.ipv6_local_subnet));
+}
+
+TEST_F(ClatdTest, DataSanitycheck) {
+  // Sanity checks the data.
+  uint8_t v4_header[] = { IPV4_UDP_HEADER };
+  ASSERT_EQ(sizeof(struct iphdr), sizeof(v4_header)) << "Test IPv4 header: incorrect length\n";
+
+  uint8_t v6_header[] = { IPV6_UDP_HEADER };
+  ASSERT_EQ(sizeof(struct ip6_hdr), sizeof(v6_header)) << "Test IPv6 header: incorrect length\n";
+
+  uint8_t udp_header[] = { UDP_HEADER };
+  ASSERT_EQ(sizeof(struct udphdr), sizeof(udp_header)) << "Test UDP header: incorrect length\n";
+
+  // Sanity checks check_packet.
+  struct udphdr *udp;
+  uint8_t v4_udp_packet[] = { IPV4_UDP_HEADER UDP_HEADER PAYLOAD };
+  udp                     = (struct udphdr *)(v4_udp_packet + sizeof(struct iphdr));
+  fix_udp_checksum(v4_udp_packet);
+  ASSERT_EQ(kUdpV4Checksum, udp->check) << "UDP/IPv4 packet checksum sanity check\n";
+  check_packet(v4_udp_packet, sizeof(v4_udp_packet), "UDP/IPv4 packet sanity check");
+
+  uint8_t v6_udp_packet[] = { IPV6_UDP_HEADER UDP_HEADER PAYLOAD };
+  udp                     = (struct udphdr *)(v6_udp_packet + sizeof(struct ip6_hdr));
+  fix_udp_checksum(v6_udp_packet);
+  ASSERT_EQ(kUdpV6Checksum, udp->check) << "UDP/IPv6 packet checksum sanity check\n";
+  check_packet(v6_udp_packet, sizeof(v6_udp_packet), "UDP/IPv6 packet sanity check");
+
+  uint8_t ipv4_ping[] = { IPV4_ICMP_HEADER IPV4_PING PAYLOAD };
+  check_packet(ipv4_ping, sizeof(ipv4_ping), "IPv4 ping sanity check");
+
+  uint8_t ipv6_ping[] = { IPV6_ICMPV6_HEADER IPV6_PING PAYLOAD };
+  check_packet(ipv6_ping, sizeof(ipv6_ping), "IPv6 ping sanity check");
+
+  // Sanity checks reassemble_packet.
+  uint8_t reassembled[MAXMTU];
+  size_t total_length = sizeof(reassembled);
+  reassemble_packet(kIPv4Fragments, kIPv4FragLengths, ARRAYSIZE(kIPv4Fragments), reassembled,
+                    &total_length, "Reassembly sanity check");
+  check_packet(reassembled, total_length, "IPv4 Reassembled packet is valid");
+  ASSERT_EQ(sizeof(kReassembledIPv4), total_length) << "IPv4 reassembly sanity check: length\n";
+  ASSERT_TRUE(!is_ipv4_fragment((struct iphdr *)reassembled))
+    << "Sanity check: reassembled packet is a fragment!\n";
+  check_data_matches(kReassembledIPv4, reassembled, total_length, "IPv4 reassembly sanity check");
+
+  total_length = sizeof(reassembled);
+  reassemble_packet(kIPv6Fragments, kIPv6FragLengths, ARRAYSIZE(kIPv6Fragments), reassembled,
+                    &total_length, "IPv6 reassembly sanity check");
+  ASSERT_TRUE(!is_ipv6_fragment((struct ip6_hdr *)reassembled, total_length))
+    << "Sanity check: reassembled packet is a fragment!\n";
+  check_packet(reassembled, total_length, "IPv6 Reassembled packet is valid");
+}
+
+TEST_F(ClatdTest, PseudoChecksum) {
+  uint32_t pseudo_checksum;
+
+  uint8_t v4_header[]        = { IPV4_UDP_HEADER };
+  uint8_t v4_pseudo_header[] = { IPV4_PSEUDOHEADER(v4_header, UDP_LEN) };
+  pseudo_checksum            = ipv4_pseudo_header_checksum((struct iphdr *)v4_header, UDP_LEN);
+  EXPECT_EQ(ip_checksum_finish(pseudo_checksum),
+            ip_checksum(v4_pseudo_header, sizeof(v4_pseudo_header)))
+    << "ipv4_pseudo_header_checksum incorrect\n";
+
+  uint8_t v6_header[]        = { IPV6_UDP_HEADER };
+  uint8_t v6_pseudo_header[] = { IPV6_PSEUDOHEADER(v6_header, IPPROTO_UDP, UDP_LEN) };
+  pseudo_checksum = ipv6_pseudo_header_checksum((struct ip6_hdr *)v6_header, UDP_LEN, IPPROTO_UDP);
+  EXPECT_EQ(ip_checksum_finish(pseudo_checksum),
+            ip_checksum(v6_pseudo_header, sizeof(v6_pseudo_header)))
+    << "ipv6_pseudo_header_checksum incorrect\n";
+}
+
+TEST_F(ClatdTest, TransportChecksum) {
+  uint8_t udphdr[]  = { UDP_HEADER };
+  uint8_t payload[] = { PAYLOAD };
+  EXPECT_EQ(kUdpPartialChecksum, ip_checksum_add(0, udphdr, sizeof(udphdr)))
+    << "UDP partial checksum\n";
+  EXPECT_EQ(kPayloadPartialChecksum, ip_checksum_add(0, payload, sizeof(payload)))
+    << "Payload partial checksum\n";
+
+  uint8_t ip[]             = { IPV4_UDP_HEADER };
+  uint8_t ip6[]            = { IPV6_UDP_HEADER };
+  uint32_t ipv4_pseudo_sum = ipv4_pseudo_header_checksum((struct iphdr *)ip, UDP_LEN);
+  uint32_t ipv6_pseudo_sum =
+    ipv6_pseudo_header_checksum((struct ip6_hdr *)ip6, UDP_LEN, IPPROTO_UDP);
+
+  EXPECT_NE(0, ipv4_pseudo_sum);
+  EXPECT_NE(0, ipv6_pseudo_sum);
+  EXPECT_EQ(0x3ad0U, ipv4_pseudo_sum % 0xFFFF) << "IPv4 pseudo-checksum sanity check\n";
+  EXPECT_EQ(0x644dU, ipv6_pseudo_sum % 0xFFFF) << "IPv6 pseudo-checksum sanity check\n";
+  EXPECT_EQ(
+      kUdpV4Checksum,
+      ip_checksum_finish(ipv4_pseudo_sum + kUdpPartialChecksum + kPayloadPartialChecksum))
+      << "Unexpected UDP/IPv4 checksum\n";
+  EXPECT_EQ(
+      kUdpV6Checksum,
+      ip_checksum_finish(ipv6_pseudo_sum + kUdpPartialChecksum + kPayloadPartialChecksum))
+      << "Unexpected UDP/IPv6 checksum\n";
+
+  EXPECT_EQ(kUdpV6Checksum,
+      ip_checksum_adjust(kUdpV4Checksum, ipv4_pseudo_sum, ipv6_pseudo_sum))
+      << "Adjust IPv4/UDP checksum to IPv6\n";
+  EXPECT_EQ(kUdpV4Checksum,
+      ip_checksum_adjust(kUdpV6Checksum, ipv6_pseudo_sum, ipv4_pseudo_sum))
+      << "Adjust IPv6/UDP checksum to IPv4\n";
+}
+
+TEST_F(ClatdTest, AdjustChecksum) {
+  struct checksum_data {
+    uint16_t checksum;
+    uint32_t old_hdr_sum;
+    uint32_t new_hdr_sum;
+    uint16_t result;
+  } DATA[] = {
+    { 0x1423, 0xb8ec, 0x2d757, 0xf5b5 },
+    { 0xf5b5, 0x2d757, 0xb8ec, 0x1423 },
+    { 0xdd2f, 0x5555, 0x3285, 0x0000 },
+    { 0x1215, 0x5560, 0x15560 + 20, 0x1200 },
+    { 0xd0c7, 0x3ad0, 0x2644b, 0xa74a },
+  };
+  unsigned i = 0;
+
+  for (i = 0; i < ARRAYSIZE(DATA); i++) {
+    struct checksum_data *data = DATA + i;
+    uint16_t result = ip_checksum_adjust(data->checksum, data->old_hdr_sum, data->new_hdr_sum);
+    EXPECT_EQ(result, data->result)
+        << "Incorrect checksum" << std::showbase << std::hex
+        << "\n  Expected: " << data->result
+        << "\n  Actual:   " << result
+        << "\n    checksum=" << data->checksum
+        << " old_sum=" << data->old_hdr_sum << " new_sum=" << data->new_hdr_sum << "\n";
+  }
+}
+
+TEST_F(ClatdTest, Translate) {
+  // This test uses hardcoded packets so the clatd address must be fixed.
+  inet_pton(AF_INET6, kIPv6LocalAddr, &Global_Clatd_Config.ipv6_local_subnet);
+
+  uint8_t udp_ipv4[] = { IPV4_UDP_HEADER UDP_HEADER PAYLOAD };
+  uint8_t udp_ipv6[] = { IPV6_UDP_HEADER UDP_HEADER PAYLOAD };
+  fix_udp_checksum(udp_ipv4);
+  fix_udp_checksum(udp_ipv6);
+  check_translated_packet(udp_ipv4, sizeof(udp_ipv4), udp_ipv6, sizeof(udp_ipv6),
+                          "UDP/IPv4 -> UDP/IPv6 translation");
+  check_translated_packet(udp_ipv6, sizeof(udp_ipv6), udp_ipv4, sizeof(udp_ipv4),
+                          "UDP/IPv6 -> UDP/IPv4 translation");
+
+  uint8_t ipv4_ping[] = { IPV4_ICMP_HEADER IPV4_PING PAYLOAD };
+  uint8_t ipv6_ping[] = { IPV6_ICMPV6_HEADER IPV6_PING PAYLOAD };
+  check_translated_packet(ipv4_ping, sizeof(ipv4_ping), ipv6_ping, sizeof(ipv6_ping),
+                          "ICMP->ICMPv6 translation");
+  check_translated_packet(ipv6_ping, sizeof(ipv6_ping), ipv4_ping, sizeof(ipv4_ping),
+                          "ICMPv6->ICMP translation");
+}
+
+TEST_F(ClatdTest, Fragmentation) {
+  // This test uses hardcoded packets so the clatd address must be fixed.
+  inet_pton(AF_INET6, kIPv6LocalAddr, &Global_Clatd_Config.ipv6_local_subnet);
+
+  check_fragment_translation(kIPv4Fragments, kIPv4FragLengths, kIPv6Fragments, kIPv6FragLengths,
+                             ARRAYSIZE(kIPv4Fragments), "IPv4->IPv6 fragment translation");
+
+  check_fragment_translation(kIPv6Fragments, kIPv6FragLengths, kIPv4Fragments, kIPv4FragLengths,
+                             ARRAYSIZE(kIPv6Fragments), "IPv6->IPv4 fragment translation");
+}
+
+// picks a random interface ID that is checksum neutral with the IPv4 address and the NAT64 prefix
+void gen_random_iid(struct in6_addr *myaddr, struct in_addr *ipv4_local_subnet,
+                    struct in6_addr *plat_subnet) {
+  // Fill last 8 bytes of IPv6 address with random bits.
+  arc4random_buf(&myaddr->s6_addr[8], 8);
+
+  // Make the IID checksum-neutral. That is, make it so that:
+  //   checksum(Local IPv4 | Remote IPv4) = checksum(Local IPv6 | Remote IPv6)
+  // in other words (because remote IPv6 = NAT64 prefix | Remote IPv4):
+  //   checksum(Local IPv4) = checksum(Local IPv6 | NAT64 prefix)
+  // Do this by adjusting the two bytes in the middle of the IID.
+
+  uint16_t middlebytes = (myaddr->s6_addr[11] << 8) + myaddr->s6_addr[12];
+
+  uint32_t c1 = ip_checksum_add(0, ipv4_local_subnet, sizeof(*ipv4_local_subnet));
+  uint32_t c2 = ip_checksum_add(0, plat_subnet, sizeof(*plat_subnet)) +
+                ip_checksum_add(0, myaddr, sizeof(*myaddr));
+
+  uint16_t delta      = ip_checksum_adjust(middlebytes, c1, c2);
+  myaddr->s6_addr[11] = delta >> 8;
+  myaddr->s6_addr[12] = delta & 0xff;
+}
+
+void check_translate_checksum_neutral(const uint8_t *original, size_t original_len,
+                                      size_t expected_len, const char *msg) {
+  uint8_t translated[MAXMTU];
+  size_t translated_len = sizeof(translated);
+  do_translate_packet(original, original_len, translated, &translated_len, msg);
+  EXPECT_EQ(expected_len, translated_len) << msg << ": Translated packet length incorrect\n";
+  // do_translate_packet already checks packets for validity and verifies the checksum.
+  int original_check   = get_transport_checksum(original);
+  int translated_check = get_transport_checksum(translated);
+  ASSERT_NE(-1, original_check);
+  ASSERT_NE(-1, translated_check);
+  ASSERT_EQ(original_check, translated_check)
+    << "Not checksum neutral: original and translated checksums differ\n";
+}
+
+TEST_F(ClatdTest, TranslateChecksumNeutral) {
+  // Generate a random clat IPv6 address and check that translation is checksum-neutral.
+  ASSERT_TRUE(inet_pton(AF_INET6, "2001:db8:1:2:f076:ae99:124e:aa54",
+                        &Global_Clatd_Config.ipv6_local_subnet));
+
+  gen_random_iid(&Global_Clatd_Config.ipv6_local_subnet, &Global_Clatd_Config.ipv4_local_subnet,
+                 &Global_Clatd_Config.plat_subnet);
+
+  ASSERT_NE(htonl((uint32_t)0x00000464), Global_Clatd_Config.ipv6_local_subnet.s6_addr32[3]);
+  ASSERT_NE((uint32_t)0, Global_Clatd_Config.ipv6_local_subnet.s6_addr32[3]);
+
+  // Check that translating UDP packets is checksum-neutral. First, IPv4.
+  uint8_t udp_ipv4[] = { IPV4_UDP_HEADER UDP_HEADER PAYLOAD };
+  fix_udp_checksum(udp_ipv4);
+  check_translate_checksum_neutral(udp_ipv4, sizeof(udp_ipv4), sizeof(udp_ipv4) + 20,
+                                   "UDP/IPv4 -> UDP/IPv6 checksum neutral");
+
+  // Now try IPv6.
+  uint8_t udp_ipv6[] = { IPV6_UDP_HEADER UDP_HEADER PAYLOAD };
+  // The test packet uses the static IID, not the random IID. Fix up the source address.
+  struct ip6_hdr *ip6 = (struct ip6_hdr *)udp_ipv6;
+  memcpy(&ip6->ip6_src, &Global_Clatd_Config.ipv6_local_subnet, sizeof(ip6->ip6_src));
+  fix_udp_checksum(udp_ipv6);
+  check_translate_checksum_neutral(udp_ipv4, sizeof(udp_ipv4), sizeof(udp_ipv4) + 20,
+                                   "UDP/IPv4 -> UDP/IPv6 checksum neutral");
+}
diff --git a/clatd/common.h b/clatd/common.h
new file mode 100644
index 0000000..e9551ee
--- /dev/null
+++ b/clatd/common.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2018 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.
+ *
+ * common.h - common definitions
+ */
+#ifndef __CLATD_COMMON_H__
+#define __CLATD_COMMON_H__
+
+#include <sys/uio.h>
+
+// A clat_packet is an array of iovec structures representing a packet that we are translating.
+// The CLAT_POS_XXX constants represent the array indices within the clat_packet that contain
+// specific parts of the packet. The packet_* functions operate on all the packet segments past a
+// given position.
+typedef enum {
+  CLAT_POS_TUNHDR,
+  CLAT_POS_IPHDR,
+  CLAT_POS_FRAGHDR,
+  CLAT_POS_TRANSPORTHDR,
+  CLAT_POS_ICMPERR_IPHDR,
+  CLAT_POS_ICMPERR_FRAGHDR,
+  CLAT_POS_ICMPERR_TRANSPORTHDR,
+  CLAT_POS_PAYLOAD,
+  CLAT_POS_MAX
+} clat_packet_index;
+typedef struct iovec clat_packet[CLAT_POS_MAX];
+
+#endif /* __CLATD_COMMON_H__ */
diff --git a/clatd/config.h b/clatd/config.h
new file mode 100644
index 0000000..9612192
--- /dev/null
+++ b/clatd/config.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2011 Daniel Drown
+ *
+ * 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.
+ *
+ * config.h - configuration settings
+ */
+#ifndef __CONFIG_H__
+#define __CONFIG_H__
+
+#include <linux/if.h>
+#include <netinet/in.h>
+
+struct tun_data {
+  char device4[IFNAMSIZ];
+  int read_fd6, write_fd6, fd4;
+};
+
+struct clat_config {
+  struct in6_addr ipv6_local_subnet;
+  struct in_addr ipv4_local_subnet;
+  struct in6_addr plat_subnet;
+  const char *native_ipv6_interface;
+};
+
+extern struct clat_config Global_Clatd_Config;
+
+/* function: ipv6_prefix_equal
+ * compares the /64 prefixes of two ipv6 addresses.
+ *   a1 - first address
+ *   a2 - second address
+ *   returns: 0 if the subnets are different, 1 if they are the same.
+ */
+static inline int ipv6_prefix_equal(struct in6_addr *a1, struct in6_addr *a2) {
+  return !memcmp(a1, a2, 8);
+}
+
+#endif /* __CONFIG_H__ */
diff --git a/clatd/debug.h b/clatd/debug.h
new file mode 100644
index 0000000..8e09672
--- /dev/null
+++ b/clatd/debug.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2011 Daniel Drown
+ *
+ * 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.
+ *
+ * debug.h - debug settings
+ */
+#ifndef __DEBUG_H__
+#define __DEBUG_H__
+
+// set to 1 to enable debug logging and packet dumping.
+#define CLAT_DEBUG 0
+
+#endif /* __DEBUG_H__ */
diff --git a/clatd/dump.c b/clatd/dump.c
new file mode 100644
index 0000000..dff3d5e
--- /dev/null
+++ b/clatd/dump.c
@@ -0,0 +1,250 @@
+/*
+ * Copyright 2011 Daniel Drown
+ *
+ * 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.
+ *
+ * dump.c - print various headers for debugging
+ */
+#include "dump.h"
+
+#include <arpa/inet.h>
+#include <linux/icmp.h>
+#include <linux/if_tun.h>
+#include <netinet/icmp6.h>
+#include <netinet/in.h>
+#include <netinet/ip.h>
+#include <netinet/ip6.h>
+#include <netinet/ip_icmp.h>
+#include <netinet/tcp.h>
+#include <netinet/udp.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "checksum.h"
+#include "clatd.h"
+#include "debug.h"
+#include "logging.h"
+
+#if CLAT_DEBUG
+
+/* print ip header */
+void dump_ip(struct iphdr *header) {
+  u_int16_t frag_flags;
+  char addrstr[INET6_ADDRSTRLEN];
+
+  frag_flags = ntohs(header->frag_off);
+
+  printf("IP packet\n");
+  printf("header_len = %x\n", header->ihl);
+  printf("version = %x\n", header->version);
+  printf("tos = %x\n", header->tos);
+  printf("tot_len = %x\n", ntohs(header->tot_len));
+  printf("id = %x\n", ntohs(header->id));
+  printf("frag: ");
+  if (frag_flags & IP_RF) {
+    printf("(RF) ");
+  }
+  if (frag_flags & IP_DF) {
+    printf("DF ");
+  }
+  if (frag_flags & IP_MF) {
+    printf("MF ");
+  }
+  printf("offset = %x\n", frag_flags & IP_OFFMASK);
+  printf("ttl = %x\n", header->ttl);
+  printf("protocol = %x\n", header->protocol);
+  printf("checksum = %x\n", ntohs(header->check));
+  inet_ntop(AF_INET, &header->saddr, addrstr, sizeof(addrstr));
+  printf("saddr = %s\n", addrstr);
+  inet_ntop(AF_INET, &header->daddr, addrstr, sizeof(addrstr));
+  printf("daddr = %s\n", addrstr);
+}
+
+/* print ip6 header */
+void dump_ip6(struct ip6_hdr *header) {
+  char addrstr[INET6_ADDRSTRLEN];
+
+  printf("ipv6\n");
+  printf("version = %x\n", header->ip6_vfc >> 4);
+  printf("traffic class = %x\n", header->ip6_flow >> 20);
+  printf("flow label = %x\n", ntohl(header->ip6_flow & 0x000fffff));
+  printf("payload len = %x\n", ntohs(header->ip6_plen));
+  printf("next header = %x\n", header->ip6_nxt);
+  printf("hop limit = %x\n", header->ip6_hlim);
+
+  inet_ntop(AF_INET6, &header->ip6_src, addrstr, sizeof(addrstr));
+  printf("source = %s\n", addrstr);
+
+  inet_ntop(AF_INET6, &header->ip6_dst, addrstr, sizeof(addrstr));
+  printf("dest = %s\n", addrstr);
+}
+
+/* print icmp header */
+void dump_icmp(struct icmphdr *icmp) {
+  printf("ICMP\n");
+
+  printf("icmp.type = %x ", icmp->type);
+  if (icmp->type == ICMP_ECHOREPLY) {
+    printf("echo reply");
+  } else if (icmp->type == ICMP_ECHO) {
+    printf("echo request");
+  } else {
+    printf("other");
+  }
+  printf("\n");
+  printf("icmp.code = %x\n", icmp->code);
+  printf("icmp.checksum = %x\n", ntohs(icmp->checksum));
+  if (icmp->type == ICMP_ECHOREPLY || icmp->type == ICMP_ECHO) {
+    printf("icmp.un.echo.id = %x\n", ntohs(icmp->un.echo.id));
+    printf("icmp.un.echo.sequence = %x\n", ntohs(icmp->un.echo.sequence));
+  }
+}
+
+/* print icmp6 header */
+void dump_icmp6(struct icmp6_hdr *icmp6) {
+  printf("ICMP6\n");
+  printf("type = %x", icmp6->icmp6_type);
+  if (icmp6->icmp6_type == ICMP6_ECHO_REQUEST) {
+    printf("(echo request)");
+  } else if (icmp6->icmp6_type == ICMP6_ECHO_REPLY) {
+    printf("(echo reply)");
+  }
+  printf("\n");
+  printf("code = %x\n", icmp6->icmp6_code);
+
+  printf("checksum = %x\n", icmp6->icmp6_cksum);
+
+  if ((icmp6->icmp6_type == ICMP6_ECHO_REQUEST) || (icmp6->icmp6_type == ICMP6_ECHO_REPLY)) {
+    printf("icmp6_id = %x\n", icmp6->icmp6_id);
+    printf("icmp6_seq = %x\n", icmp6->icmp6_seq);
+  }
+}
+
+/* print udp header */
+void dump_udp_generic(const struct udphdr *udp, uint32_t temp_checksum, const uint8_t *payload,
+                      size_t payload_size) {
+  uint16_t my_checksum;
+
+  temp_checksum = ip_checksum_add(temp_checksum, udp, sizeof(struct udphdr));
+  temp_checksum = ip_checksum_add(temp_checksum, payload, payload_size);
+  my_checksum   = ip_checksum_finish(temp_checksum);
+
+  printf("UDP\n");
+  printf("source = %x\n", ntohs(udp->source));
+  printf("dest = %x\n", ntohs(udp->dest));
+  printf("len = %x\n", ntohs(udp->len));
+  printf("check = %x (mine %x)\n", udp->check, my_checksum);
+}
+
+/* print ipv4/udp header */
+void dump_udp(const struct udphdr *udp, const struct iphdr *ip, const uint8_t *payload,
+              size_t payload_size) {
+  uint32_t temp_checksum;
+  temp_checksum = ipv4_pseudo_header_checksum(ip, sizeof(*udp) + payload_size);
+  dump_udp_generic(udp, temp_checksum, payload, payload_size);
+}
+
+/* print ipv6/udp header */
+void dump_udp6(const struct udphdr *udp, const struct ip6_hdr *ip6, const uint8_t *payload,
+               size_t payload_size) {
+  uint32_t temp_checksum;
+  temp_checksum = ipv6_pseudo_header_checksum(ip6, sizeof(*udp) + payload_size, IPPROTO_UDP);
+  dump_udp_generic(udp, temp_checksum, payload, payload_size);
+}
+
+/* print tcp header */
+void dump_tcp_generic(const struct tcphdr *tcp, const uint8_t *options, size_t options_size,
+                      uint32_t temp_checksum, const uint8_t *payload, size_t payload_size) {
+  uint16_t my_checksum;
+
+  temp_checksum = ip_checksum_add(temp_checksum, tcp, sizeof(struct tcphdr));
+  if (options) {
+    temp_checksum = ip_checksum_add(temp_checksum, options, options_size);
+  }
+  temp_checksum = ip_checksum_add(temp_checksum, payload, payload_size);
+  my_checksum   = ip_checksum_finish(temp_checksum);
+
+  printf("TCP\n");
+  printf("source = %x\n", ntohs(tcp->source));
+  printf("dest = %x\n", ntohs(tcp->dest));
+  printf("seq = %x\n", ntohl(tcp->seq));
+  printf("ack = %x\n", ntohl(tcp->ack_seq));
+  printf("d_off = %x\n", tcp->doff);
+  printf("res1 = %x\n", tcp->res1);
+#ifdef __BIONIC__
+  printf("CWR = %x\n", tcp->cwr);
+  printf("ECE = %x\n", tcp->ece);
+#else
+  printf("CWR/ECE = %x\n", tcp->res2);
+#endif
+  printf("urg = %x  ack = %x  psh = %x  rst = %x  syn = %x  fin = %x\n", tcp->urg, tcp->ack,
+         tcp->psh, tcp->rst, tcp->syn, tcp->fin);
+  printf("window = %x\n", ntohs(tcp->window));
+  printf("check = %x [mine %x]\n", tcp->check, my_checksum);
+  printf("urgent = %x\n", tcp->urg_ptr);
+
+  if (options) {
+    size_t i;
+
+    printf("options: ");
+    for (i = 0; i < options_size; i++) {
+      printf("%x ", *(options + i));
+    }
+    printf("\n");
+  }
+}
+
+/* print ipv4/tcp header */
+void dump_tcp(const struct tcphdr *tcp, const struct iphdr *ip, const uint8_t *payload,
+              size_t payload_size, const uint8_t *options, size_t options_size) {
+  uint32_t temp_checksum;
+
+  temp_checksum = ipv4_pseudo_header_checksum(ip, sizeof(*tcp) + options_size + payload_size);
+  dump_tcp_generic(tcp, options, options_size, temp_checksum, payload, payload_size);
+}
+
+/* print ipv6/tcp header */
+void dump_tcp6(const struct tcphdr *tcp, const struct ip6_hdr *ip6, const uint8_t *payload,
+               size_t payload_size, const uint8_t *options, size_t options_size) {
+  uint32_t temp_checksum;
+
+  temp_checksum =
+    ipv6_pseudo_header_checksum(ip6, sizeof(*tcp) + options_size + payload_size, IPPROTO_TCP);
+  dump_tcp_generic(tcp, options, options_size, temp_checksum, payload, payload_size);
+}
+
+/* generic hex dump */
+void logcat_hexdump(const char *info, const uint8_t *data, size_t len) {
+  char output[MAXDUMPLEN * 3 + 2];
+  size_t i;
+
+  output[0] = '\0';
+  for (i = 0; i < len && i < MAXDUMPLEN; i++) {
+    snprintf(output + i * 3, 4, " %02x", data[i]);
+  }
+  output[len * 3 + 3] = '\0';
+
+  logmsg(ANDROID_LOG_WARN, "info %s len %d data%s", info, len, output);
+}
+
+void dump_iovec(const struct iovec *iov, int iov_len) {
+  int i;
+  char *str;
+  for (i = 0; i < iov_len; i++) {
+    asprintf(&str, "iov[%d]: ", i);
+    logcat_hexdump(str, iov[i].iov_base, iov[i].iov_len);
+    free(str);
+  }
+}
+#endif  // CLAT_DEBUG
diff --git a/clatd/dump.h b/clatd/dump.h
new file mode 100644
index 0000000..6b96cd2
--- /dev/null
+++ b/clatd/dump.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2011 Daniel Drown
+ *
+ * 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.
+ *
+ * dump.h - debug functions
+ */
+#ifndef __DUMP_H__
+#define __DUMP_H__
+
+#include <netinet/icmp6.h>
+#include <netinet/in.h>
+#include <netinet/ip.h>
+#include <netinet/ip6.h>
+#include <netinet/ip_icmp.h>
+#include <netinet/tcp.h>
+#include <netinet/udp.h>
+
+void dump_ip(struct iphdr *header);
+void dump_icmp(struct icmphdr *icmp);
+void dump_udp(const struct udphdr *udp, const struct iphdr *ip, const uint8_t *payload,
+              size_t payload_size);
+void dump_tcp(const struct tcphdr *tcp, const struct iphdr *ip, const uint8_t *payload,
+              size_t payload_size, const uint8_t *options, size_t options_size);
+
+void dump_ip6(struct ip6_hdr *header);
+void dump_icmp6(struct icmp6_hdr *icmp6);
+void dump_udp6(const struct udphdr *udp, const struct ip6_hdr *ip6, const uint8_t *payload,
+               size_t payload_size);
+void dump_tcp6(const struct tcphdr *tcp, const struct ip6_hdr *ip6, const uint8_t *payload,
+               size_t payload_size, const uint8_t *options, size_t options_size);
+
+void logcat_hexdump(const char *info, const uint8_t *data, size_t len);
+void dump_iovec(const struct iovec *iov, int iov_len);
+
+#endif /* __DUMP_H__ */
diff --git a/clatd/icmp.c b/clatd/icmp.c
new file mode 100644
index 0000000..f9ba113
--- /dev/null
+++ b/clatd/icmp.c
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2013 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.
+ *
+ * icmp.c - convenience functions for translating ICMP and ICMPv6 packets.
+ */
+
+#include <linux/icmp.h>
+#include <netinet/icmp6.h>
+#include <netinet/in.h>
+#include <netinet/ip_icmp.h>
+
+#include "icmp.h"
+#include "logging.h"
+
+/* function: icmp_guess_ttl
+ * Guesses the number of hops a received packet has traversed based on its TTL.
+ * ttl - the ttl of the received packet.
+ */
+uint8_t icmp_guess_ttl(uint8_t ttl) {
+  if (ttl > 128) {
+    return 255 - ttl;
+  } else if (ttl > 64) {
+    return 128 - ttl;
+  } else if (ttl > 32) {
+    return 64 - ttl;
+  } else {
+    return 32 - ttl;
+  }
+}
+
+/* function: is_icmp_error
+ * Determines whether an ICMP type is an error message.
+ * type: the ICMP type
+ */
+int is_icmp_error(uint8_t type) { return type == 3 || type == 11 || type == 12; }
+
+/* function: is_icmp6_error
+ * Determines whether an ICMPv6 type is an error message.
+ * type: the ICMPv6 type
+ */
+int is_icmp6_error(uint8_t type) { return type < 128; }
+
+/* function: icmp_to_icmp6_type
+ * Maps ICMP types to ICMPv6 types. Partial implementation of RFC 6145, section 4.2.
+ * type - the ICMPv6 type
+ */
+uint8_t icmp_to_icmp6_type(uint8_t type, uint8_t code) {
+  switch (type) {
+    case ICMP_ECHO:
+      return ICMP6_ECHO_REQUEST;
+
+    case ICMP_ECHOREPLY:
+      return ICMP6_ECHO_REPLY;
+
+    case ICMP_TIME_EXCEEDED:
+      return ICMP6_TIME_EXCEEDED;
+
+    case ICMP_DEST_UNREACH:
+      // These two types need special translation which we don't support yet.
+      if (code != ICMP_UNREACH_PROTOCOL && code != ICMP_UNREACH_NEEDFRAG) {
+        return ICMP6_DST_UNREACH;
+      }
+  }
+
+  // We don't understand this ICMP type. Return parameter problem so the caller will bail out.
+  logmsg_dbg(ANDROID_LOG_DEBUG, "icmp_to_icmp6_type: unhandled ICMP type %d", type);
+  return ICMP6_PARAM_PROB;
+}
+
+/* function: icmp_to_icmp6_code
+ * Maps ICMP codes to ICMPv6 codes. Partial implementation of RFC 6145, section 4.2.
+ * type - the ICMP type
+ * code - the ICMP code
+ */
+uint8_t icmp_to_icmp6_code(uint8_t type, uint8_t code) {
+  switch (type) {
+    case ICMP_ECHO:
+    case ICMP_ECHOREPLY:
+      return 0;
+
+    case ICMP_TIME_EXCEEDED:
+      return code;
+
+    case ICMP_DEST_UNREACH:
+      switch (code) {
+        case ICMP_UNREACH_NET:
+        case ICMP_UNREACH_HOST:
+          return ICMP6_DST_UNREACH_NOROUTE;
+
+        case ICMP_UNREACH_PORT:
+          return ICMP6_DST_UNREACH_NOPORT;
+
+        case ICMP_UNREACH_NET_PROHIB:
+        case ICMP_UNREACH_HOST_PROHIB:
+        case ICMP_UNREACH_FILTER_PROHIB:
+        case ICMP_UNREACH_PRECEDENCE_CUTOFF:
+          return ICMP6_DST_UNREACH_ADMIN;
+
+          // Otherwise, we don't understand this ICMP type/code combination. Fall through.
+      }
+  }
+  logmsg_dbg(ANDROID_LOG_DEBUG, "icmp_to_icmp6_code: unhandled ICMP type/code %d/%d", type, code);
+  return 0;
+}
+
+/* function: icmp6_to_icmp_type
+ * Maps ICMPv6 types to ICMP types. Partial implementation of RFC 6145, section 5.2.
+ * type - the ICMP type
+ */
+uint8_t icmp6_to_icmp_type(uint8_t type, uint8_t code) {
+  switch (type) {
+    case ICMP6_ECHO_REQUEST:
+      return ICMP_ECHO;
+
+    case ICMP6_ECHO_REPLY:
+      return ICMP_ECHOREPLY;
+
+    case ICMP6_DST_UNREACH:
+      return ICMP_DEST_UNREACH;
+
+    case ICMP6_TIME_EXCEEDED:
+      return ICMP_TIME_EXCEEDED;
+  }
+
+  // We don't understand this ICMP type. Return parameter problem so the caller will bail out.
+  logmsg_dbg(ANDROID_LOG_DEBUG, "icmp6_to_icmp_type: unhandled ICMP type/code %d/%d", type, code);
+  return ICMP_PARAMETERPROB;
+}
+
+/* function: icmp6_to_icmp_code
+ * Maps ICMPv6 codes to ICMP codes. Partial implementation of RFC 6145, section 5.2.
+ * type - the ICMPv6 type
+ * code - the ICMPv6 code
+ */
+uint8_t icmp6_to_icmp_code(uint8_t type, uint8_t code) {
+  switch (type) {
+    case ICMP6_ECHO_REQUEST:
+    case ICMP6_ECHO_REPLY:
+    case ICMP6_TIME_EXCEEDED:
+      return code;
+
+    case ICMP6_DST_UNREACH:
+      switch (code) {
+        case ICMP6_DST_UNREACH_NOROUTE:
+          return ICMP_UNREACH_HOST;
+
+        case ICMP6_DST_UNREACH_ADMIN:
+          return ICMP_UNREACH_HOST_PROHIB;
+
+        case ICMP6_DST_UNREACH_BEYONDSCOPE:
+          return ICMP_UNREACH_HOST;
+
+        case ICMP6_DST_UNREACH_ADDR:
+          return ICMP_HOST_UNREACH;
+
+        case ICMP6_DST_UNREACH_NOPORT:
+          return ICMP_UNREACH_PORT;
+
+          // Otherwise, we don't understand this ICMPv6 type/code combination. Fall through.
+      }
+  }
+
+  logmsg_dbg(ANDROID_LOG_DEBUG, "icmp6_to_icmp_code: unhandled ICMP type/code %d/%d", type, code);
+  return 0;
+}
diff --git a/clatd/icmp.h b/clatd/icmp.h
new file mode 100644
index 0000000..632e92d
--- /dev/null
+++ b/clatd/icmp.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2013 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.
+ *
+ * icmp.c - convenience functions for translating ICMP and ICMPv6 packets.
+ */
+
+#ifndef __ICMP_H__
+#define __ICMP_H__
+
+#include <stdint.h>
+
+// Guesses the number of hops a received packet has traversed based on its TTL.
+uint8_t icmp_guess_ttl(uint8_t ttl);
+
+// Determines whether an ICMP type is an error message.
+int is_icmp_error(uint8_t type);
+
+// Determines whether an ICMPv6 type is an error message.
+int is_icmp6_error(uint8_t type);
+
+// Maps ICMP types to ICMPv6 types. Partial implementation of RFC 6145, section 4.2.
+uint8_t icmp_to_icmp6_type(uint8_t type, uint8_t code);
+
+// Maps ICMP codes to ICMPv6 codes. Partial implementation of RFC 6145, section 4.2.
+uint8_t icmp_to_icmp6_code(uint8_t type, uint8_t code);
+
+// Maps ICMPv6 types to ICMP types. Partial implementation of RFC 6145, section 5.2.
+uint8_t icmp6_to_icmp_type(uint8_t type, uint8_t code);
+
+// Maps ICMPv6 codes to ICMP codes. Partial implementation of RFC 6145, section 5.2.
+uint8_t icmp6_to_icmp_code(uint8_t type, uint8_t code);
+
+#endif /* __ICMP_H__ */
diff --git a/clatd/ipv4.c b/clatd/ipv4.c
new file mode 100644
index 0000000..2be02e3
--- /dev/null
+++ b/clatd/ipv4.c
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2011 Daniel Drown
+ *
+ * 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.
+ *
+ * ipv4.c - takes ipv4 packets, finds their headers, and then calls translation functions on them
+ */
+#include <string.h>
+
+#include "checksum.h"
+#include "debug.h"
+#include "dump.h"
+#include "logging.h"
+#include "translate.h"
+
+/* function: icmp_packet
+ * translates an icmp packet
+ * out      - output packet
+ * icmp     - pointer to icmp header in packet
+ * checksum - pseudo-header checksum
+ * len      - size of ip payload
+ * returns: the highest position in the output clat_packet that's filled in
+ */
+int icmp_packet(clat_packet out, clat_packet_index pos, const struct icmphdr *icmp,
+                uint32_t checksum, size_t len) {
+  const uint8_t *payload;
+  size_t payload_size;
+
+  if (len < sizeof(struct icmphdr)) {
+    logmsg_dbg(ANDROID_LOG_ERROR, "icmp_packet/(too small)");
+    return 0;
+  }
+
+  payload      = (const uint8_t *)(icmp + 1);
+  payload_size = len - sizeof(struct icmphdr);
+
+  return icmp_to_icmp6(out, pos, icmp, checksum, payload, payload_size);
+}
+
+/* function: ipv4_packet
+ * translates an ipv4 packet
+ * out    - output packet
+ * packet - packet data
+ * len    - size of packet
+ * returns: the highest position in the output clat_packet that's filled in
+ */
+int ipv4_packet(clat_packet out, clat_packet_index pos, const uint8_t *packet, size_t len) {
+  const struct iphdr *header = (struct iphdr *)packet;
+  struct ip6_hdr *ip6_targ   = (struct ip6_hdr *)out[pos].iov_base;
+  struct ip6_frag *frag_hdr;
+  size_t frag_hdr_len;
+  uint8_t nxthdr;
+  const uint8_t *next_header;
+  size_t len_left;
+  uint32_t old_sum, new_sum;
+  int iov_len;
+
+  if (len < sizeof(struct iphdr)) {
+    logmsg_dbg(ANDROID_LOG_ERROR, "ip_packet/too short for an ip header");
+    return 0;
+  }
+
+  if (header->ihl < 5) {
+    logmsg_dbg(ANDROID_LOG_ERROR, "ip_packet/ip header length set to less than 5: %x", header->ihl);
+    return 0;
+  }
+
+  if ((size_t)header->ihl * 4 > len) {  // ip header length larger than entire packet
+    logmsg_dbg(ANDROID_LOG_ERROR, "ip_packet/ip header length set too large: %x", header->ihl);
+    return 0;
+  }
+
+  if (header->version != 4) {
+    logmsg_dbg(ANDROID_LOG_ERROR, "ip_packet/ip header version not 4: %x", header->version);
+    return 0;
+  }
+
+  /* rfc6145 - If any IPv4 options are present in the IPv4 packet, they MUST be
+   * ignored and the packet translated normally; there is no attempt to
+   * translate the options.
+   */
+
+  next_header = packet + header->ihl * 4;
+  len_left    = len - header->ihl * 4;
+
+  nxthdr = header->protocol;
+  if (nxthdr == IPPROTO_ICMP) {
+    // ICMP and ICMPv6 have different protocol numbers.
+    nxthdr = IPPROTO_ICMPV6;
+  }
+
+  /* Fill in the IPv6 header. We need to do this before we translate the packet because TCP and
+   * UDP include parts of the IP header in the checksum. Set the length to zero because we don't
+   * know it yet.
+   */
+  fill_ip6_header(ip6_targ, 0, nxthdr, header);
+  out[pos].iov_len = sizeof(struct ip6_hdr);
+
+  /* Calculate the pseudo-header checksum.
+   * Technically, the length that is used in the pseudo-header checksum is the transport layer
+   * length, which is not the same as len_left in the case of fragmented packets. But since
+   * translation does not change the transport layer length, the checksum is unaffected.
+   */
+  old_sum = ipv4_pseudo_header_checksum(header, len_left);
+  new_sum = ipv6_pseudo_header_checksum(ip6_targ, len_left, nxthdr);
+
+  // If the IPv4 packet is fragmented, add a Fragment header.
+  frag_hdr             = (struct ip6_frag *)out[pos + 1].iov_base;
+  frag_hdr_len         = maybe_fill_frag_header(frag_hdr, ip6_targ, header);
+  out[pos + 1].iov_len = frag_hdr_len;
+
+  if (frag_hdr_len && frag_hdr->ip6f_offlg & IP6F_OFF_MASK) {
+    // Non-first fragment. Copy the rest of the packet as is.
+    iov_len = generic_packet(out, pos + 2, next_header, len_left);
+  } else if (nxthdr == IPPROTO_ICMPV6) {
+    iov_len = icmp_packet(out, pos + 2, (const struct icmphdr *)next_header, new_sum, len_left);
+  } else if (nxthdr == IPPROTO_TCP) {
+    iov_len =
+      tcp_packet(out, pos + 2, (const struct tcphdr *)next_header, old_sum, new_sum, len_left);
+  } else if (nxthdr == IPPROTO_UDP) {
+    iov_len =
+      udp_packet(out, pos + 2, (const struct udphdr *)next_header, old_sum, new_sum, len_left);
+  } else if (nxthdr == IPPROTO_GRE || nxthdr == IPPROTO_ESP) {
+    iov_len = generic_packet(out, pos + 2, next_header, len_left);
+  } else {
+#if CLAT_DEBUG
+    logmsg_dbg(ANDROID_LOG_ERROR, "ip_packet/unknown protocol: %x", header->protocol);
+    logcat_hexdump("ipv4/protocol", packet, len);
+#endif
+    return 0;
+  }
+
+  // Set the length.
+  ip6_targ->ip6_plen = htons(packet_length(out, pos));
+  return iov_len;
+}
diff --git a/clatd/ipv6.c b/clatd/ipv6.c
new file mode 100644
index 0000000..05cd3ab
--- /dev/null
+++ b/clatd/ipv6.c
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2011 Daniel Drown
+ *
+ * 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.
+ *
+ * ipv6.c - takes ipv6 packets, finds their headers, and then calls translation functions on them
+ */
+#include <arpa/inet.h>
+#include <string.h>
+
+#include "checksum.h"
+#include "config.h"
+#include "debug.h"
+#include "dump.h"
+#include "logging.h"
+#include "translate.h"
+
+/* function: icmp6_packet
+ * takes an icmp6 packet and sets it up for translation
+ * out      - output packet
+ * icmp6    - pointer to icmp6 header in packet
+ * checksum - pseudo-header checksum (unused)
+ * len      - size of ip payload
+ * returns: the highest position in the output clat_packet that's filled in
+ */
+int icmp6_packet(clat_packet out, clat_packet_index pos, const struct icmp6_hdr *icmp6,
+                 size_t len) {
+  const uint8_t *payload;
+  size_t payload_size;
+
+  if (len < sizeof(struct icmp6_hdr)) {
+    logmsg_dbg(ANDROID_LOG_ERROR, "icmp6_packet/(too small)");
+    return 0;
+  }
+
+  payload      = (const uint8_t *)(icmp6 + 1);
+  payload_size = len - sizeof(struct icmp6_hdr);
+
+  return icmp6_to_icmp(out, pos, icmp6, payload, payload_size);
+}
+
+/* function: log_bad_address
+ * logs a bad address to android's log buffer if debugging is turned on
+ * fmt     - printf-style format, use %s to place the address
+ * badaddr - the bad address in question
+ */
+#if CLAT_DEBUG
+void log_bad_address(const char *fmt, const struct in6_addr *src, const struct in6_addr *dst) {
+  char srcstr[INET6_ADDRSTRLEN];
+  char dststr[INET6_ADDRSTRLEN];
+
+  inet_ntop(AF_INET6, src, srcstr, sizeof(srcstr));
+  inet_ntop(AF_INET6, dst, dststr, sizeof(dststr));
+  logmsg_dbg(ANDROID_LOG_ERROR, fmt, srcstr, dststr);
+}
+#else
+#define log_bad_address(fmt, src, dst)
+#endif
+
+/* function: ipv6_packet
+ * takes an ipv6 packet and hands it off to the layer 4 protocol function
+ * out    - output packet
+ * packet - packet data
+ * len    - size of packet
+ * returns: the highest position in the output clat_packet that's filled in
+ */
+int ipv6_packet(clat_packet out, clat_packet_index pos, const uint8_t *packet, size_t len) {
+  const struct ip6_hdr *ip6 = (struct ip6_hdr *)packet;
+  struct iphdr *ip_targ     = (struct iphdr *)out[pos].iov_base;
+  struct ip6_frag *frag_hdr = NULL;
+  uint8_t protocol;
+  const uint8_t *next_header;
+  size_t len_left;
+  uint32_t old_sum, new_sum;
+  int iov_len;
+
+  if (len < sizeof(struct ip6_hdr)) {
+    logmsg_dbg(ANDROID_LOG_ERROR, "ipv6_packet/too short for an ip6 header: %d", len);
+    return 0;
+  }
+
+  if (IN6_IS_ADDR_MULTICAST(&ip6->ip6_dst)) {
+    log_bad_address("ipv6_packet/multicast %s->%s", &ip6->ip6_src, &ip6->ip6_dst);
+    return 0;  // silently ignore
+  }
+
+  // If the packet is not from the plat subnet to the local subnet, or vice versa, drop it, unless
+  // it's an ICMP packet (which can come from anywhere). We do not send IPv6 packets from the plat
+  // subnet to the local subnet, but these can appear as inner packets in ICMP errors, so we need
+  // to translate them. We accept third-party ICMPv6 errors, even though their source addresses
+  // cannot be translated, so that things like unreachables and traceroute will work. fill_ip_header
+  // takes care of faking a source address for them.
+  if (!(is_in_plat_subnet(&ip6->ip6_src) &&
+        IN6_ARE_ADDR_EQUAL(&ip6->ip6_dst, &Global_Clatd_Config.ipv6_local_subnet)) &&
+      !(is_in_plat_subnet(&ip6->ip6_dst) &&
+        IN6_ARE_ADDR_EQUAL(&ip6->ip6_src, &Global_Clatd_Config.ipv6_local_subnet)) &&
+      ip6->ip6_nxt != IPPROTO_ICMPV6) {
+    log_bad_address("ipv6_packet/wrong source address: %s->%s", &ip6->ip6_src, &ip6->ip6_dst);
+    return 0;
+  }
+
+  next_header = packet + sizeof(struct ip6_hdr);
+  len_left    = len - sizeof(struct ip6_hdr);
+
+  protocol = ip6->ip6_nxt;
+
+  /* Fill in the IPv4 header. We need to do this before we translate the packet because TCP and
+   * UDP include parts of the IP header in the checksum. Set the length to zero because we don't
+   * know it yet.
+   */
+  fill_ip_header(ip_targ, 0, protocol, ip6);
+  out[pos].iov_len = sizeof(struct iphdr);
+
+  // If there's a Fragment header, parse it and decide what the next header is.
+  // Do this before calculating the pseudo-header checksum because it updates the next header value.
+  if (protocol == IPPROTO_FRAGMENT) {
+    frag_hdr = (struct ip6_frag *)next_header;
+    if (len_left < sizeof(*frag_hdr)) {
+      logmsg_dbg(ANDROID_LOG_ERROR, "ipv6_packet/too short for fragment header: %d", len);
+      return 0;
+    }
+
+    next_header += sizeof(*frag_hdr);
+    len_left -= sizeof(*frag_hdr);
+
+    protocol = parse_frag_header(frag_hdr, ip_targ);
+  }
+
+  // ICMP and ICMPv6 have different protocol numbers.
+  if (protocol == IPPROTO_ICMPV6) {
+    protocol          = IPPROTO_ICMP;
+    ip_targ->protocol = IPPROTO_ICMP;
+  }
+
+  /* Calculate the pseudo-header checksum.
+   * Technically, the length that is used in the pseudo-header checksum is the transport layer
+   * length, which is not the same as len_left in the case of fragmented packets. But since
+   * translation does not change the transport layer length, the checksum is unaffected.
+   */
+  old_sum = ipv6_pseudo_header_checksum(ip6, len_left, protocol);
+  new_sum = ipv4_pseudo_header_checksum(ip_targ, len_left);
+
+  // Does not support IPv6 extension headers except Fragment.
+  if (frag_hdr && (frag_hdr->ip6f_offlg & IP6F_OFF_MASK)) {
+    iov_len = generic_packet(out, pos + 2, next_header, len_left);
+  } else if (protocol == IPPROTO_ICMP) {
+    iov_len = icmp6_packet(out, pos + 2, (const struct icmp6_hdr *)next_header, len_left);
+  } else if (protocol == IPPROTO_TCP) {
+    iov_len =
+      tcp_packet(out, pos + 2, (const struct tcphdr *)next_header, old_sum, new_sum, len_left);
+  } else if (protocol == IPPROTO_UDP) {
+    iov_len =
+      udp_packet(out, pos + 2, (const struct udphdr *)next_header, old_sum, new_sum, len_left);
+  } else if (protocol == IPPROTO_GRE || protocol == IPPROTO_ESP) {
+    iov_len = generic_packet(out, pos + 2, next_header, len_left);
+  } else {
+#if CLAT_DEBUG
+    logmsg(ANDROID_LOG_ERROR, "ipv6_packet/unknown next header type: %x", ip6->ip6_nxt);
+    logcat_hexdump("ipv6/nxthdr", packet, len);
+#endif
+    return 0;
+  }
+
+  // Set the length and calculate the checksum.
+  ip_targ->tot_len = htons(ntohs(ip_targ->tot_len) + packet_length(out, pos));
+  ip_targ->check   = ip_checksum(ip_targ, sizeof(struct iphdr));
+  return iov_len;
+}
diff --git a/clatd/logging.c b/clatd/logging.c
new file mode 100644
index 0000000..79d98e1
--- /dev/null
+++ b/clatd/logging.c
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2011 Daniel Drown
+ *
+ * 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.
+ *
+ * logging.c - print a log message
+ */
+
+#include <android/log.h>
+#include <stdarg.h>
+
+#include "debug.h"
+#include "logging.h"
+
+/* function: logmsg
+ * prints a log message to android's log buffer
+ * prio - the log message priority
+ * fmt  - printf format specifier
+ * ...  - printf format arguments
+ */
+void logmsg(int prio, const char *fmt, ...) {
+  va_list ap;
+
+  va_start(ap, fmt);
+  __android_log_vprint(prio, "clatd", fmt, ap);
+  va_end(ap);
+}
+
+/* function: logmsg_dbg
+ * prints a log message to android's log buffer if CLAT_DEBUG is set
+ * prio - the log message priority
+ * fmt  - printf format specifier
+ * ...  - printf format arguments
+ */
+#if CLAT_DEBUG
+void logmsg_dbg(int prio, const char *fmt, ...) {
+  va_list ap;
+
+  va_start(ap, fmt);
+  __android_log_vprint(prio, "clatd", fmt, ap);
+  va_end(ap);
+}
+#else
+void logmsg_dbg(__attribute__((unused)) int prio, __attribute__((unused)) const char *fmt, ...) {}
+#endif
diff --git a/clatd/logging.h b/clatd/logging.h
new file mode 100644
index 0000000..1f4b6b6
--- /dev/null
+++ b/clatd/logging.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2011 Daniel Drown
+ *
+ * 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.
+ *
+ * logging.h - print a log message
+ */
+
+#ifndef __LOGGING_H__
+#define __LOGGING_H__
+// for the priorities
+#include <android/log.h>
+
+void logmsg(int prio, const char *fmt, ...);
+void logmsg_dbg(int prio, const char *fmt, ...);
+
+#endif
diff --git a/clatd/main.c b/clatd/main.c
new file mode 100644
index 0000000..f888041
--- /dev/null
+++ b/clatd/main.c
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2018 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.
+ *
+ * main.c - main function
+ */
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <netinet/in.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/personality.h>
+#include <sys/utsname.h>
+#include <unistd.h>
+
+#include "clatd.h"
+#include "common.h"
+#include "config.h"
+#include "logging.h"
+
+#define DEVICEPREFIX "v4-"
+
+/* function: stop_loop
+ * signal handler: stop the event loop
+ */
+static void stop_loop() { running = 0; };
+
+/* function: print_help
+ * in case the user is running this on the command line
+ */
+void print_help() {
+  printf("android-clat arguments:\n");
+  printf("-i [uplink interface]\n");
+  printf("-p [plat prefix]\n");
+  printf("-4 [IPv4 address]\n");
+  printf("-6 [IPv6 address]\n");
+  printf("-t [tun file descriptor number]\n");
+  printf("-r [read socket descriptor number]\n");
+  printf("-w [write socket descriptor number]\n");
+}
+
+/* function: main
+ * allocate and setup the tun device, then run the event loop
+ */
+int main(int argc, char **argv) {
+  struct tun_data tunnel;
+  int opt;
+  char *uplink_interface = NULL, *plat_prefix = NULL;
+  char *v4_addr = NULL, *v6_addr = NULL, *tunfd_str = NULL, *read_sock_str = NULL,
+       *write_sock_str = NULL;
+  unsigned len;
+
+  while ((opt = getopt(argc, argv, "i:p:4:6:t:r:w:h")) != -1) {
+    switch (opt) {
+      case 'i':
+        uplink_interface = optarg;
+        break;
+      case 'p':
+        plat_prefix = optarg;
+        break;
+      case '4':
+        v4_addr = optarg;
+        break;
+      case '6':
+        v6_addr = optarg;
+        break;
+      case 't':
+        tunfd_str = optarg;
+        break;
+      case 'r':
+        read_sock_str = optarg;
+        break;
+      case 'w':
+        write_sock_str = optarg;
+        break;
+      case 'h':
+        print_help();
+        exit(0);
+      default:
+        logmsg(ANDROID_LOG_FATAL, "Unknown option -%c. Exiting.", (char)optopt);
+        exit(1);
+    }
+  }
+
+  if (uplink_interface == NULL) {
+    logmsg(ANDROID_LOG_FATAL, "clatd called without an interface");
+    exit(1);
+  }
+
+  if (tunfd_str != NULL && !parse_int(tunfd_str, &tunnel.fd4)) {
+    logmsg(ANDROID_LOG_FATAL, "invalid tunfd %s", tunfd_str);
+    exit(1);
+  }
+  if (!tunnel.fd4) {
+    logmsg(ANDROID_LOG_FATAL, "no tunfd specified on commandline.");
+    exit(1);
+  }
+
+  if (read_sock_str != NULL && !parse_int(read_sock_str, &tunnel.read_fd6)) {
+    logmsg(ANDROID_LOG_FATAL, "invalid read socket %s", read_sock_str);
+    exit(1);
+  }
+  if (!tunnel.read_fd6) {
+    logmsg(ANDROID_LOG_FATAL, "no read_fd6 specified on commandline.");
+    exit(1);
+  }
+
+  if (write_sock_str != NULL && !parse_int(write_sock_str, &tunnel.write_fd6)) {
+    logmsg(ANDROID_LOG_FATAL, "invalid write socket %s", write_sock_str);
+    exit(1);
+  }
+  if (!tunnel.write_fd6) {
+    logmsg(ANDROID_LOG_FATAL, "no write_fd6 specified on commandline.");
+    exit(1);
+  }
+
+  len = snprintf(tunnel.device4, sizeof(tunnel.device4), "%s%s", DEVICEPREFIX, uplink_interface);
+  if (len >= sizeof(tunnel.device4)) {
+    logmsg(ANDROID_LOG_FATAL, "interface name too long '%s'", tunnel.device4);
+    exit(1);
+  }
+
+  Global_Clatd_Config.native_ipv6_interface = uplink_interface;
+  if (!plat_prefix || inet_pton(AF_INET6, plat_prefix, &Global_Clatd_Config.plat_subnet) <= 0) {
+    logmsg(ANDROID_LOG_FATAL, "invalid IPv6 address specified for plat prefix: %s", plat_prefix);
+    exit(1);
+  }
+
+  if (!v4_addr || !inet_pton(AF_INET, v4_addr, &Global_Clatd_Config.ipv4_local_subnet.s_addr)) {
+    logmsg(ANDROID_LOG_FATAL, "Invalid IPv4 address %s", v4_addr);
+    exit(1);
+  }
+
+  if (!v6_addr || !inet_pton(AF_INET6, v6_addr, &Global_Clatd_Config.ipv6_local_subnet)) {
+    logmsg(ANDROID_LOG_FATAL, "Invalid source address %s", v6_addr);
+    exit(1);
+  }
+
+  logmsg(ANDROID_LOG_INFO, "Starting clat version %s on %s plat=%s v4=%s v6=%s", CLATD_VERSION,
+         uplink_interface, plat_prefix ? plat_prefix : "(none)", v4_addr ? v4_addr : "(none)",
+         v6_addr ? v6_addr : "(none)");
+
+  {
+    // Compile time detection of 32 vs 64-bit build. (note: C does not have 'constexpr')
+    // Avoid use of preprocessor macros to get compile time syntax checking even on 64-bit.
+    const int user_bits = sizeof(void*) * 8;
+    const bool user32 = (user_bits == 32);
+
+    // Note that on 64-bit all this personality related code simply compile optimizes out.
+    // 32-bit: fetch current personality (see 'man personality': 0xFFFFFFFF means retrieve only)
+    // On Linux fetching personality cannot fail.
+    const int prev_personality = user32 ? personality(0xFFFFFFFFuL) : PER_LINUX;
+    // 32-bit: attempt to get rid of kernel spoofing of 'uts.machine' architecture,
+    // In theory this cannot fail, as PER_LINUX should always be supported.
+    if (user32) (void)personality((prev_personality & ~PER_MASK) | PER_LINUX);
+    // 64-bit: this will compile time evaluate to false.
+    const bool was_linux32 = (prev_personality & PER_MASK) == PER_LINUX32;
+
+    struct utsname uts = {};
+    if (uname(&uts)) exit(1); // only possible error is EFAULT, but 'uts' is on stack
+
+    // sysname is likely 'Linux', release is 'kver', machine is kernel's *true* architecture
+    logmsg(ANDROID_LOG_INFO, "%d-bit userspace on %s kernel %s for %s%s.", user_bits,
+           uts.sysname, uts.release, uts.machine, was_linux32 ? " (was spoofed)" : "");
+
+    // 32-bit: try to return to the 'default' personality
+    // In theory this cannot fail, because it was already previously in use.
+    if (user32) (void)personality(prev_personality);
+  }
+
+  // Loop until someone sends us a signal or brings down the tun interface.
+  if (signal(SIGTERM, stop_loop) == SIG_ERR) {
+    logmsg(ANDROID_LOG_FATAL, "sigterm handler failed: %s", strerror(errno));
+    exit(1);
+  }
+
+  event_loop(&tunnel);
+
+  logmsg(ANDROID_LOG_INFO, "Shutting down clat on %s", uplink_interface);
+
+  if (running) {
+    logmsg(ANDROID_LOG_INFO, "Clatd on %s waiting for SIGTERM", uplink_interface);
+    // let's give higher level java code 15 seconds to kill us,
+    // but eventually terminate anyway, in case system server forgets about us...
+    // sleep() should be interrupted by SIGTERM, the handler should clear running
+    sleep(15);
+    logmsg(ANDROID_LOG_INFO, "Clatd on %s %s SIGTERM", uplink_interface,
+           running ? "timed out waiting for" : "received");
+  } else {
+    logmsg(ANDROID_LOG_INFO, "Clatd on %s already received SIGTERM", uplink_interface);
+  }
+  return 0;
+}
diff --git a/clatd/translate.c b/clatd/translate.c
new file mode 100644
index 0000000..22830d89
--- /dev/null
+++ b/clatd/translate.c
@@ -0,0 +1,529 @@
+/*
+ * Copyright 2011 Daniel Drown
+ *
+ * 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.
+ *
+ * translate.c - CLAT functions / partial implementation of rfc6145
+ */
+#include "translate.h"
+
+#include <string.h>
+
+#include "checksum.h"
+#include "clatd.h"
+#include "common.h"
+#include "config.h"
+#include "debug.h"
+#include "icmp.h"
+#include "logging.h"
+
+/* function: packet_checksum
+ * calculates the checksum over all the packet components starting from pos
+ * checksum - checksum of packet components before pos
+ * packet   - packet to calculate the checksum of
+ * pos      - position to start counting from
+ * returns  - the completed 16-bit checksum, ready to write into a checksum header field
+ */
+uint16_t packet_checksum(uint32_t checksum, clat_packet packet, clat_packet_index pos) {
+  int i;
+  for (i = pos; i < CLAT_POS_MAX; i++) {
+    if (packet[i].iov_len > 0) {
+      checksum = ip_checksum_add(checksum, packet[i].iov_base, packet[i].iov_len);
+    }
+  }
+  return ip_checksum_finish(checksum);
+}
+
+/* function: packet_length
+ * returns the total length of all the packet components after pos
+ * packet - packet to calculate the length of
+ * pos    - position to start counting after
+ * returns: the total length of the packet components after pos
+ */
+uint16_t packet_length(clat_packet packet, clat_packet_index pos) {
+  size_t len = 0;
+  int i;
+  for (i = pos + 1; i < CLAT_POS_MAX; i++) {
+    len += packet[i].iov_len;
+  }
+  return len;
+}
+
+/* function: is_in_plat_subnet
+ * returns true iff the given IPv6 address is in the plat subnet.
+ * addr - IPv6 address
+ */
+int is_in_plat_subnet(const struct in6_addr *addr6) {
+  // Assumes a /96 plat subnet.
+  return (addr6 != NULL) && (memcmp(addr6, &Global_Clatd_Config.plat_subnet, 12) == 0);
+}
+
+/* function: ipv6_addr_to_ipv4_addr
+ * return the corresponding ipv4 address for the given ipv6 address
+ * addr6 - ipv6 address
+ * returns: the IPv4 address
+ */
+uint32_t ipv6_addr_to_ipv4_addr(const struct in6_addr *addr6) {
+  if (is_in_plat_subnet(addr6)) {
+    // Assumes a /96 plat subnet.
+    return addr6->s6_addr32[3];
+  } else if (IN6_ARE_ADDR_EQUAL(addr6, &Global_Clatd_Config.ipv6_local_subnet)) {
+    // Special-case our own address.
+    return Global_Clatd_Config.ipv4_local_subnet.s_addr;
+  } else {
+    // Third party packet. Let the caller deal with it.
+    return INADDR_NONE;
+  }
+}
+
+/* function: ipv4_addr_to_ipv6_addr
+ * return the corresponding ipv6 address for the given ipv4 address
+ * addr4 - ipv4 address
+ */
+struct in6_addr ipv4_addr_to_ipv6_addr(uint32_t addr4) {
+  struct in6_addr addr6;
+  // Both addresses are in network byte order (addr4 comes from a network packet, and the config
+  // file entry is read using inet_ntop).
+  if (addr4 == Global_Clatd_Config.ipv4_local_subnet.s_addr) {
+    return Global_Clatd_Config.ipv6_local_subnet;
+  } else {
+    // Assumes a /96 plat subnet.
+    addr6              = Global_Clatd_Config.plat_subnet;
+    addr6.s6_addr32[3] = addr4;
+    return addr6;
+  }
+}
+
+/* function: fill_tun_header
+ * fill in the header for the tun fd
+ * tun_header - tunnel header, already allocated
+ * proto      - ethernet protocol id: ETH_P_IP(ipv4) or ETH_P_IPV6(ipv6)
+ */
+void fill_tun_header(struct tun_pi *tun_header, uint16_t proto) {
+  tun_header->flags = 0;
+  tun_header->proto = htons(proto);
+}
+
+/* function: fill_ip_header
+ * generate an ipv4 header from an ipv6 header
+ * ip_targ     - (ipv4) target packet header, source: original ipv4 addr, dest: local subnet addr
+ * payload_len - length of other data inside packet
+ * protocol    - protocol number (tcp, udp, etc)
+ * old_header  - (ipv6) source packet header, source: nat64 prefix, dest: local subnet prefix
+ */
+void fill_ip_header(struct iphdr *ip, uint16_t payload_len, uint8_t protocol,
+                    const struct ip6_hdr *old_header) {
+  int ttl_guess;
+  memset(ip, 0, sizeof(struct iphdr));
+
+  ip->ihl      = 5;
+  ip->version  = 4;
+  ip->tos      = 0;
+  ip->tot_len  = htons(sizeof(struct iphdr) + payload_len);
+  ip->id       = 0;
+  ip->frag_off = htons(IP_DF);
+  ip->ttl      = old_header->ip6_hlim;
+  ip->protocol = protocol;
+  ip->check    = 0;
+
+  ip->saddr = ipv6_addr_to_ipv4_addr(&old_header->ip6_src);
+  ip->daddr = ipv6_addr_to_ipv4_addr(&old_header->ip6_dst);
+
+  // Third-party ICMPv6 message. This may have been originated by an native IPv6 address.
+  // In that case, the source IPv6 address can't be translated and we need to make up an IPv4
+  // source address. For now, use 255.0.0.<ttl>, which at least looks useful in traceroute.
+  if ((uint32_t)ip->saddr == INADDR_NONE) {
+    ttl_guess = icmp_guess_ttl(old_header->ip6_hlim);
+    ip->saddr = htonl((0xff << 24) + ttl_guess);
+  }
+}
+
+/* function: fill_ip6_header
+ * generate an ipv6 header from an ipv4 header
+ * ip6         - (ipv6) target packet header, source: local subnet prefix, dest: nat64 prefix
+ * payload_len - length of other data inside packet
+ * protocol    - protocol number (tcp, udp, etc)
+ * old_header  - (ipv4) source packet header, source: local subnet addr, dest: internet's ipv4 addr
+ */
+void fill_ip6_header(struct ip6_hdr *ip6, uint16_t payload_len, uint8_t protocol,
+                     const struct iphdr *old_header) {
+  memset(ip6, 0, sizeof(struct ip6_hdr));
+
+  ip6->ip6_vfc  = 6 << 4;
+  ip6->ip6_plen = htons(payload_len);
+  ip6->ip6_nxt  = protocol;
+  ip6->ip6_hlim = old_header->ttl;
+
+  ip6->ip6_src = ipv4_addr_to_ipv6_addr(old_header->saddr);
+  ip6->ip6_dst = ipv4_addr_to_ipv6_addr(old_header->daddr);
+}
+
+/* function: maybe_fill_frag_header
+ * fills a fragmentation header
+ * generate an ipv6 fragment header from an ipv4 header
+ * frag_hdr    - target (ipv6) fragmentation header
+ * ip6_targ    - target (ipv6) header
+ * old_header  - (ipv4) source packet header
+ * returns: the length of the fragmentation header if present, or zero if not present
+ */
+size_t maybe_fill_frag_header(struct ip6_frag *frag_hdr, struct ip6_hdr *ip6_targ,
+                              const struct iphdr *old_header) {
+  uint16_t frag_flags = ntohs(old_header->frag_off);
+  uint16_t frag_off   = frag_flags & IP_OFFMASK;
+  if (frag_off == 0 && (frag_flags & IP_MF) == 0) {
+    // Not a fragment.
+    return 0;
+  }
+
+  frag_hdr->ip6f_nxt      = ip6_targ->ip6_nxt;
+  frag_hdr->ip6f_reserved = 0;
+  // In IPv4, the offset is the bottom 13 bits; in IPv6 it's the top 13 bits.
+  frag_hdr->ip6f_offlg = htons(frag_off << 3);
+  if (frag_flags & IP_MF) {
+    frag_hdr->ip6f_offlg |= IP6F_MORE_FRAG;
+  }
+  frag_hdr->ip6f_ident = htonl(ntohs(old_header->id));
+  ip6_targ->ip6_nxt    = IPPROTO_FRAGMENT;
+
+  return sizeof(*frag_hdr);
+}
+
+/* function: parse_frag_header
+ * return the length of the fragmentation header if present, or zero if not present
+ * generate an ipv6 fragment header from an ipv4 header
+ * frag_hdr    - (ipv6) fragmentation header
+ * ip_targ     - target (ipv4) header
+ * returns: the next header value
+ */
+uint8_t parse_frag_header(const struct ip6_frag *frag_hdr, struct iphdr *ip_targ) {
+  uint16_t frag_off = (ntohs(frag_hdr->ip6f_offlg & IP6F_OFF_MASK) >> 3);
+  if (frag_hdr->ip6f_offlg & IP6F_MORE_FRAG) {
+    frag_off |= IP_MF;
+  }
+  ip_targ->frag_off = htons(frag_off);
+  ip_targ->id       = htons(ntohl(frag_hdr->ip6f_ident) & 0xffff);
+  ip_targ->protocol = frag_hdr->ip6f_nxt;
+  return frag_hdr->ip6f_nxt;
+}
+
+/* function: icmp_to_icmp6
+ * translate ipv4 icmp to ipv6 icmp
+ * out          - output packet
+ * icmp         - source packet icmp header
+ * checksum     - pseudo-header checksum
+ * payload      - icmp payload
+ * payload_size - size of payload
+ * returns: the highest position in the output clat_packet that's filled in
+ */
+int icmp_to_icmp6(clat_packet out, clat_packet_index pos, const struct icmphdr *icmp,
+                  uint32_t checksum, const uint8_t *payload, size_t payload_size) {
+  struct icmp6_hdr *icmp6_targ = out[pos].iov_base;
+  uint8_t icmp6_type;
+  int clat_packet_len;
+
+  memset(icmp6_targ, 0, sizeof(struct icmp6_hdr));
+
+  icmp6_type             = icmp_to_icmp6_type(icmp->type, icmp->code);
+  icmp6_targ->icmp6_type = icmp6_type;
+  icmp6_targ->icmp6_code = icmp_to_icmp6_code(icmp->type, icmp->code);
+
+  out[pos].iov_len = sizeof(struct icmp6_hdr);
+
+  if (pos == CLAT_POS_TRANSPORTHDR && is_icmp_error(icmp->type) && icmp6_type != ICMP6_PARAM_PROB) {
+    // An ICMP error we understand, one level deep.
+    // Translate the nested packet (the one that caused the error).
+    clat_packet_len = ipv4_packet(out, pos + 1, payload, payload_size);
+
+    // The pseudo-header checksum was calculated on the transport length of the original IPv4
+    // packet that we were asked to translate. This transport length is 20 bytes smaller than it
+    // needs to be, because the ICMP error contains an IPv4 header, which we will be translating to
+    // an IPv6 header, which is 20 bytes longer. Fix it up here.
+    // We only need to do this for ICMP->ICMPv6, not ICMPv6->ICMP, because ICMP does not use the
+    // pseudo-header when calculating its checksum (as the IPv4 header has its own checksum).
+    checksum = checksum + htons(20);
+  } else if (icmp6_type == ICMP6_ECHO_REQUEST || icmp6_type == ICMP6_ECHO_REPLY) {
+    // Ping packet.
+    icmp6_targ->icmp6_id           = icmp->un.echo.id;
+    icmp6_targ->icmp6_seq          = icmp->un.echo.sequence;
+    out[CLAT_POS_PAYLOAD].iov_base = (uint8_t *)payload;
+    out[CLAT_POS_PAYLOAD].iov_len  = payload_size;
+    clat_packet_len                = CLAT_POS_PAYLOAD + 1;
+  } else {
+    // Unknown type/code. The type/code conversion functions have already logged an error.
+    return 0;
+  }
+
+  icmp6_targ->icmp6_cksum = 0;  // Checksum field must be 0 when calculating checksum.
+  icmp6_targ->icmp6_cksum = packet_checksum(checksum, out, pos);
+
+  return clat_packet_len;
+}
+
+/* function: icmp6_to_icmp
+ * translate ipv6 icmp to ipv4 icmp
+ * out          - output packet
+ * icmp6        - source packet icmp6 header
+ * payload      - icmp6 payload
+ * payload_size - size of payload
+ * returns: the highest position in the output clat_packet that's filled in
+ */
+int icmp6_to_icmp(clat_packet out, clat_packet_index pos, const struct icmp6_hdr *icmp6,
+                  const uint8_t *payload, size_t payload_size) {
+  struct icmphdr *icmp_targ = out[pos].iov_base;
+  uint8_t icmp_type;
+  int clat_packet_len;
+
+  memset(icmp_targ, 0, sizeof(struct icmphdr));
+
+  icmp_type       = icmp6_to_icmp_type(icmp6->icmp6_type, icmp6->icmp6_code);
+  icmp_targ->type = icmp_type;
+  icmp_targ->code = icmp6_to_icmp_code(icmp6->icmp6_type, icmp6->icmp6_code);
+
+  out[pos].iov_len = sizeof(struct icmphdr);
+
+  if (pos == CLAT_POS_TRANSPORTHDR && is_icmp6_error(icmp6->icmp6_type) &&
+      icmp_type != ICMP_PARAMETERPROB) {
+    // An ICMPv6 error we understand, one level deep.
+    // Translate the nested packet (the one that caused the error).
+    clat_packet_len = ipv6_packet(out, pos + 1, payload, payload_size);
+  } else if (icmp_type == ICMP_ECHO || icmp_type == ICMP_ECHOREPLY) {
+    // Ping packet.
+    icmp_targ->un.echo.id          = icmp6->icmp6_id;
+    icmp_targ->un.echo.sequence    = icmp6->icmp6_seq;
+    out[CLAT_POS_PAYLOAD].iov_base = (uint8_t *)payload;
+    out[CLAT_POS_PAYLOAD].iov_len  = payload_size;
+    clat_packet_len                = CLAT_POS_PAYLOAD + 1;
+  } else {
+    // Unknown type/code. The type/code conversion functions have already logged an error.
+    return 0;
+  }
+
+  icmp_targ->checksum = 0;  // Checksum field must be 0 when calculating checksum.
+  icmp_targ->checksum = packet_checksum(0, out, pos);
+
+  return clat_packet_len;
+}
+
+/* function: generic_packet
+ * takes a generic IP packet and sets it up for translation
+ * out      - output packet
+ * pos      - position in the output packet of the transport header
+ * payload  - pointer to IP payload
+ * len      - size of ip payload
+ * returns: the highest position in the output clat_packet that's filled in
+ */
+int generic_packet(clat_packet out, clat_packet_index pos, const uint8_t *payload, size_t len) {
+  out[pos].iov_len               = 0;
+  out[CLAT_POS_PAYLOAD].iov_base = (uint8_t *)payload;
+  out[CLAT_POS_PAYLOAD].iov_len  = len;
+
+  return CLAT_POS_PAYLOAD + 1;
+}
+
+/* function: udp_packet
+ * takes a udp packet and sets it up for translation
+ * out      - output packet
+ * udp      - pointer to udp header in packet
+ * old_sum  - pseudo-header checksum of old header
+ * new_sum  - pseudo-header checksum of new header
+ * len      - size of ip payload
+ */
+int udp_packet(clat_packet out, clat_packet_index pos, const struct udphdr *udp, uint32_t old_sum,
+               uint32_t new_sum, size_t len) {
+  const uint8_t *payload;
+  size_t payload_size;
+
+  if (len < sizeof(struct udphdr)) {
+    logmsg_dbg(ANDROID_LOG_ERROR, "udp_packet/(too small)");
+    return 0;
+  }
+
+  payload      = (const uint8_t *)(udp + 1);
+  payload_size = len - sizeof(struct udphdr);
+
+  return udp_translate(out, pos, udp, old_sum, new_sum, payload, payload_size);
+}
+
+/* function: tcp_packet
+ * takes a tcp packet and sets it up for translation
+ * out      - output packet
+ * tcp      - pointer to tcp header in packet
+ * checksum - pseudo-header checksum
+ * len      - size of ip payload
+ * returns: the highest position in the output clat_packet that's filled in
+ */
+int tcp_packet(clat_packet out, clat_packet_index pos, const struct tcphdr *tcp, uint32_t old_sum,
+               uint32_t new_sum, size_t len) {
+  const uint8_t *payload;
+  size_t payload_size, header_size;
+
+  if (len < sizeof(struct tcphdr)) {
+    logmsg_dbg(ANDROID_LOG_ERROR, "tcp_packet/(too small)");
+    return 0;
+  }
+
+  if (tcp->doff < 5) {
+    logmsg_dbg(ANDROID_LOG_ERROR, "tcp_packet/tcp header length set to less than 5: %x", tcp->doff);
+    return 0;
+  }
+
+  if ((size_t)tcp->doff * 4 > len) {
+    logmsg_dbg(ANDROID_LOG_ERROR, "tcp_packet/tcp header length set too large: %x", tcp->doff);
+    return 0;
+  }
+
+  header_size  = tcp->doff * 4;
+  payload      = ((const uint8_t *)tcp) + header_size;
+  payload_size = len - header_size;
+
+  return tcp_translate(out, pos, tcp, header_size, old_sum, new_sum, payload, payload_size);
+}
+
+/* function: udp_translate
+ * common between ipv4/ipv6 - setup checksum and send udp packet
+ * out          - output packet
+ * udp          - udp header
+ * old_sum      - pseudo-header checksum of old header
+ * new_sum      - pseudo-header checksum of new header
+ * payload      - tcp payload
+ * payload_size - size of payload
+ * returns: the highest position in the output clat_packet that's filled in
+ */
+int udp_translate(clat_packet out, clat_packet_index pos, const struct udphdr *udp,
+                  uint32_t old_sum, uint32_t new_sum, const uint8_t *payload, size_t payload_size) {
+  struct udphdr *udp_targ = out[pos].iov_base;
+
+  memcpy(udp_targ, udp, sizeof(struct udphdr));
+
+  out[pos].iov_len               = sizeof(struct udphdr);
+  out[CLAT_POS_PAYLOAD].iov_base = (uint8_t *)payload;
+  out[CLAT_POS_PAYLOAD].iov_len  = payload_size;
+
+  if (udp_targ->check) {
+    udp_targ->check = ip_checksum_adjust(udp->check, old_sum, new_sum);
+  } else {
+    // Zero checksums are special. RFC 768 says, "An all zero transmitted checksum value means that
+    // the transmitter generated no checksum (for debugging or for higher level protocols that
+    // don't care)." However, in IPv6 zero UDP checksums were only permitted by RFC 6935 (2013). So
+    // for safety we recompute it.
+    udp_targ->check = 0;  // Checksum field must be 0 when calculating checksum.
+    udp_targ->check = packet_checksum(new_sum, out, pos);
+  }
+
+  // RFC 768: "If the computed checksum is zero, it is transmitted as all ones (the equivalent
+  // in one's complement arithmetic)."
+  if (!udp_targ->check) {
+    udp_targ->check = 0xffff;
+  }
+
+  return CLAT_POS_PAYLOAD + 1;
+}
+
+/* function: tcp_translate
+ * common between ipv4/ipv6 - setup checksum and send tcp packet
+ * out          - output packet
+ * tcp          - tcp header
+ * header_size  - size of tcp header including options
+ * checksum     - partial checksum covering ipv4/ipv6 header
+ * payload      - tcp payload
+ * payload_size - size of payload
+ * returns: the highest position in the output clat_packet that's filled in
+ */
+int tcp_translate(clat_packet out, clat_packet_index pos, const struct tcphdr *tcp,
+                  size_t header_size, uint32_t old_sum, uint32_t new_sum, const uint8_t *payload,
+                  size_t payload_size) {
+  struct tcphdr *tcp_targ = out[pos].iov_base;
+  out[pos].iov_len        = header_size;
+
+  if (header_size > MAX_TCP_HDR) {
+    // A TCP header cannot be more than MAX_TCP_HDR bytes long because it's a 4-bit field that
+    // counts in 4-byte words. So this can never happen unless there is a bug in the caller.
+    logmsg(ANDROID_LOG_ERROR, "tcp_translate: header too long %d > %d, truncating", header_size,
+           MAX_TCP_HDR);
+    header_size = MAX_TCP_HDR;
+  }
+
+  memcpy(tcp_targ, tcp, header_size);
+
+  out[CLAT_POS_PAYLOAD].iov_base = (uint8_t *)payload;
+  out[CLAT_POS_PAYLOAD].iov_len  = payload_size;
+
+  tcp_targ->check = ip_checksum_adjust(tcp->check, old_sum, new_sum);
+
+  return CLAT_POS_PAYLOAD + 1;
+}
+
+// Weak symbol so we can override it in the unit test.
+void send_rawv6(int fd, clat_packet out, int iov_len) __attribute__((weak));
+
+void send_rawv6(int fd, clat_packet out, int iov_len) {
+  // A send on a raw socket requires a destination address to be specified even if the socket's
+  // protocol is IPPROTO_RAW. This is the address that will be used in routing lookups; the
+  // destination address in the packet header only affects what appears on the wire, not where the
+  // packet is sent to.
+  static struct sockaddr_in6 sin6 = { AF_INET6, 0, 0, { { { 0, 0, 0, 0 } } }, 0 };
+  static struct msghdr msg        = {
+    .msg_name    = &sin6,
+    .msg_namelen = sizeof(sin6),
+  };
+
+  msg.msg_iov = out, msg.msg_iovlen = iov_len,
+  sin6.sin6_addr = ((struct ip6_hdr *)out[CLAT_POS_IPHDR].iov_base)->ip6_dst;
+  sendmsg(fd, &msg, 0);
+}
+
+/* function: translate_packet
+ * takes a packet, translates it, and writes it to fd
+ * fd         - fd to write translated packet to
+ * to_ipv6    - true if translating to ipv6, false if translating to ipv4
+ * packet     - packet
+ * packetsize - size of packet
+ */
+void translate_packet(int fd, int to_ipv6, const uint8_t *packet, size_t packetsize) {
+  int iov_len = 0;
+
+  // Allocate buffers for all packet headers.
+  struct tun_pi tun_targ;
+  char iphdr[sizeof(struct ip6_hdr)];
+  char fraghdr[sizeof(struct ip6_frag)];
+  char transporthdr[MAX_TCP_HDR];
+  char icmp_iphdr[sizeof(struct ip6_hdr)];
+  char icmp_fraghdr[sizeof(struct ip6_frag)];
+  char icmp_transporthdr[MAX_TCP_HDR];
+
+  // iovec of the packets we'll send. This gets passed down to the translation functions.
+  clat_packet out = {
+    { &tun_targ, 0 },          // Tunnel header.
+    { iphdr, 0 },              // IP header.
+    { fraghdr, 0 },            // Fragment header.
+    { transporthdr, 0 },       // Transport layer header.
+    { icmp_iphdr, 0 },         // ICMP error inner IP header.
+    { icmp_fraghdr, 0 },       // ICMP error fragmentation header.
+    { icmp_transporthdr, 0 },  // ICMP error transport layer header.
+    { NULL, 0 },               // Payload. No buffer, it's a pointer to the original payload.
+  };
+
+  if (to_ipv6) {
+    iov_len = ipv4_packet(out, CLAT_POS_IPHDR, packet, packetsize);
+    if (iov_len > 0) {
+      send_rawv6(fd, out, iov_len);
+    }
+  } else {
+    iov_len = ipv6_packet(out, CLAT_POS_IPHDR, packet, packetsize);
+    if (iov_len > 0) {
+      fill_tun_header(&tun_targ, ETH_P_IP);
+      out[CLAT_POS_TUNHDR].iov_len = sizeof(tun_targ);
+      writev(fd, out, iov_len);
+    }
+  }
+}
diff --git a/clatd/translate.h b/clatd/translate.h
new file mode 100644
index 0000000..0e520f7
--- /dev/null
+++ b/clatd/translate.h
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2011 Daniel Drown
+ *
+ * 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.
+ *
+ * translate.h - translate from one version of ip to another
+ */
+#ifndef __TRANSLATE_H__
+#define __TRANSLATE_H__
+
+#include <linux/icmp.h>
+#include <linux/if_tun.h>
+#include <netinet/icmp6.h>
+#include <netinet/in.h>
+#include <netinet/ip.h>
+#include <netinet/ip6.h>
+#include <netinet/ip_icmp.h>
+#include <netinet/tcp.h>
+#include <netinet/udp.h>
+
+#include "clatd.h"
+#include "common.h"
+
+#define MAX_TCP_HDR (15 * 4)  // Data offset field is 4 bits and counts in 32-bit words.
+
+// Calculates the checksum over all the packet components starting from pos.
+uint16_t packet_checksum(uint32_t checksum, clat_packet packet, clat_packet_index pos);
+
+// Returns the total length of the packet components after pos.
+uint16_t packet_length(clat_packet packet, clat_packet_index pos);
+
+// Returns true iff the given IPv6 address is in the plat subnet.
+int is_in_plat_subnet(const struct in6_addr *addr6);
+
+// Functions to create tun, IPv4, and IPv6 headers.
+void fill_tun_header(struct tun_pi *tun_header, uint16_t proto);
+void fill_ip_header(struct iphdr *ip_targ, uint16_t payload_len, uint8_t protocol,
+                    const struct ip6_hdr *old_header);
+void fill_ip6_header(struct ip6_hdr *ip6, uint16_t payload_len, uint8_t protocol,
+                     const struct iphdr *old_header);
+
+// Translate and send packets.
+void translate_packet(int fd, int to_ipv6, const uint8_t *packet, size_t packetsize);
+
+// Translate IPv4 and IPv6 packets.
+int ipv4_packet(clat_packet out, clat_packet_index pos, const uint8_t *packet, size_t len);
+int ipv6_packet(clat_packet out, clat_packet_index pos, const uint8_t *packet, size_t len);
+
+// Deal with fragmented packets.
+size_t maybe_fill_frag_header(struct ip6_frag *frag_hdr, struct ip6_hdr *ip6_targ,
+                              const struct iphdr *old_header);
+uint8_t parse_frag_header(const struct ip6_frag *frag_hdr, struct iphdr *ip_targ);
+
+// Deal with fragmented packets.
+size_t maybe_fill_frag_header(struct ip6_frag *frag_hdr, struct ip6_hdr *ip6_targ,
+                              const struct iphdr *old_header);
+uint8_t parse_frag_header(const struct ip6_frag *frag_hdr, struct iphdr *ip_targ);
+
+// Translate ICMP packets.
+int icmp_to_icmp6(clat_packet out, clat_packet_index pos, const struct icmphdr *icmp,
+                  uint32_t checksum, const uint8_t *payload, size_t payload_size);
+int icmp6_to_icmp(clat_packet out, clat_packet_index pos, const struct icmp6_hdr *icmp6,
+                  const uint8_t *payload, size_t payload_size);
+
+// Translate generic IP packets.
+int generic_packet(clat_packet out, clat_packet_index pos, const uint8_t *payload, size_t len);
+
+// Translate TCP and UDP packets.
+int tcp_packet(clat_packet out, clat_packet_index pos, const struct tcphdr *tcp, uint32_t old_sum,
+               uint32_t new_sum, size_t len);
+int udp_packet(clat_packet out, clat_packet_index pos, const struct udphdr *udp, uint32_t old_sum,
+               uint32_t new_sum, size_t len);
+
+int tcp_translate(clat_packet out, clat_packet_index pos, const struct tcphdr *tcp,
+                  size_t header_size, uint32_t old_sum, uint32_t new_sum, const uint8_t *payload,
+                  size_t payload_size);
+int udp_translate(clat_packet out, clat_packet_index pos, const struct udphdr *udp,
+                  uint32_t old_sum, uint32_t new_sum, const uint8_t *payload, size_t payload_size);
+
+#endif /* __TRANSLATE_H__ */
diff --git a/common/Android.bp b/common/Android.bp
index ff4de11..e10ede8 100644
--- a/common/Android.bp
+++ b/common/Android.bp
@@ -19,6 +19,10 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+// This is a placeholder comment to avoid merge conflicts
+// as the above target may not exist
+// depending on the branch
+
 java_library {
     name: "connectivity-net-module-utils-bpf",
     srcs: [
diff --git a/common/TrunkStable.bp b/common/TrunkStable.bp
new file mode 100644
index 0000000..56938fc
--- /dev/null
+++ b/common/TrunkStable.bp
@@ -0,0 +1,26 @@
+//
+// 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.
+//
+
+aconfig_declarations {
+    name: "com.android.net.flags-aconfig",
+    package: "com.android.net.flags",
+    srcs: ["flags.aconfig"],
+}
+
+java_aconfig_library {
+    name: "connectivity_flags_aconfig_lib",
+    aconfig_declarations: "com.android.net.flags-aconfig",
+}
diff --git a/common/flags.aconfig b/common/flags.aconfig
new file mode 100644
index 0000000..cadc44f
--- /dev/null
+++ b/common/flags.aconfig
@@ -0,0 +1,15 @@
+package: "com.android.net.flags"
+
+flag {
+  name: "track_multiple_network_activities"
+  namespace: "android_core_networking"
+  description: "NetworkActivityTracker tracks multiple networks including non default networks"
+  bug: "267870186"
+}
+
+flag {
+  name: "forbidden_capability"
+  namespace: "android_core_networking"
+  description: "This flag controls the forbidden capability API"
+  bug: "302997505"
+}
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index b8a0e39..2f7db87 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -51,7 +51,7 @@
         ":framework-connectivity-tiramisu-updatable-sources",
         ":framework-nearby-java-sources",
         ":framework-thread-sources",
-    ] + framework_remoteauth_srcs,
+    ],
     libs: [
         "unsupportedappusage",
         "app-compat-annotations",
@@ -126,7 +126,6 @@
         "enable-framework-connectivity-t-targets",
         "FlaggedApiDefaults",
     ],
-    api_srcs: framework_remoteauth_api_srcs,
     // Do not add static_libs to this library: put them in framework-connectivity instead.
     // The jarjar rules are only so that references to jarjared utils in
     // framework-connectivity-pre-jarjar match at runtime.
@@ -143,10 +142,8 @@
         "android.net",
         "android.net.nsd",
         "android.nearby",
-        "android.remoteauth",
         "com.android.connectivity",
         "com.android.nearby",
-        "com.android.remoteauth",
     ],
 
     hidden_api: {
diff --git a/framework-t/api/OWNERS b/framework-t/api/OWNERS
index af583c3..607f85a 100644
--- a/framework-t/api/OWNERS
+++ b/framework-t/api/OWNERS
@@ -1,2 +1,2 @@
-file:platform/packages/modules/Connectivity:master:/nearby/OWNERS
-file:platform/packages/modules/Connectivity:master:/remoteauth/OWNERS
+file:platform/packages/modules/Connectivity:main:/nearby/OWNERS
+file:platform/packages/modules/Connectivity:main:/remoteauth/OWNERS
diff --git a/framework-t/api/current.txt b/framework-t/api/current.txt
index 86745d4..e4b211f 100644
--- a/framework-t/api/current.txt
+++ b/framework-t/api/current.txt
@@ -1,4 +1,6 @@
 // Signature format: 2.0
+// - add-additional-overrides=no
+// - migrating=Migration in progress see b/299366704
 package android.app.usage {
 
   public final class NetworkStats implements java.lang.AutoCloseable {
diff --git a/framework-t/api/module-lib-current.txt b/framework-t/api/module-lib-current.txt
index 5a8d47b..fd42a37 100644
--- a/framework-t/api/module-lib-current.txt
+++ b/framework-t/api/module-lib-current.txt
@@ -1,4 +1,6 @@
 // Signature format: 2.0
+// - add-additional-overrides=no
+// - migrating=Migration in progress see b/299366704
 package android.app.usage {
 
   public class NetworkStatsManager {
diff --git a/framework-t/api/module-lib-removed.txt b/framework-t/api/module-lib-removed.txt
index d802177..14191eb 100644
--- a/framework-t/api/module-lib-removed.txt
+++ b/framework-t/api/module-lib-removed.txt
@@ -1 +1,3 @@
 // Signature format: 2.0
+// - add-additional-overrides=no
+// - migrating=Migration in progress see b/299366704
diff --git a/framework-t/api/removed.txt b/framework-t/api/removed.txt
index 1ba87d8..d9d243d 100644
--- a/framework-t/api/removed.txt
+++ b/framework-t/api/removed.txt
@@ -1,4 +1,6 @@
 // Signature format: 2.0
+// - add-additional-overrides=no
+// - migrating=Migration in progress see b/299366704
 package android.net {
 
   public class TrafficStats {
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index 1549089..9bdb595 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -1,4 +1,6 @@
 // Signature format: 2.0
+// - add-additional-overrides=no
+// - migrating=Migration in progress see b/299366704
 package android.app.usage {
 
   public class NetworkStatsManager {
@@ -414,3 +416,16 @@
 
 }
 
+package android.net.thread {
+
+  public class ThreadNetworkController {
+    method public int getThreadVersion();
+    field public static final int THREAD_VERSION_1_3 = 4; // 0x4
+  }
+
+  public class ThreadNetworkManager {
+    method @NonNull public java.util.List<android.net.thread.ThreadNetworkController> getAllThreadNetworkControllers();
+  }
+
+}
+
diff --git a/framework-t/api/system-removed.txt b/framework-t/api/system-removed.txt
index d802177..14191eb 100644
--- a/framework-t/api/system-removed.txt
+++ b/framework-t/api/system-removed.txt
@@ -1 +1,3 @@
 // Signature format: 2.0
+// - add-additional-overrides=no
+// - migrating=Migration in progress see b/299366704
diff --git a/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java b/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
index d9c9d74..d89964d 100644
--- a/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
+++ b/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
@@ -24,6 +24,8 @@
 import android.net.nsd.INsdManager;
 import android.net.nsd.MDnsManager;
 import android.net.nsd.NsdManager;
+import android.net.thread.IThreadNetworkManager;
+import android.net.thread.ThreadNetworkManager;
 
 /**
  * Class for performing registration for Connectivity services which are exposed via updatable APIs
@@ -89,5 +91,14 @@
                     return new MDnsManager(service);
                 }
         );
+
+        SystemServiceRegistry.registerContextAwareService(
+                ThreadNetworkManager.SERVICE_NAME,
+                ThreadNetworkManager.class,
+                (context, serviceBinder) -> {
+                    IThreadNetworkManager managerService =
+                            IThreadNetworkManager.Stub.asInterface(serviceBinder);
+                    return new ThreadNetworkManager(context, managerService);
+                });
     }
 }
diff --git a/framework-t/src/android/net/nsd/OffloadServiceInfo.java b/framework-t/src/android/net/nsd/OffloadServiceInfo.java
index 2c839bc..d5dbf19 100644
--- a/framework-t/src/android/net/nsd/OffloadServiceInfo.java
+++ b/framework-t/src/android/net/nsd/OffloadServiceInfo.java
@@ -19,7 +19,9 @@
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresApi;
 import android.annotation.SystemApi;
+import android.os.Build;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -38,6 +40,7 @@
  * @hide
  */
 @SystemApi
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public final class OffloadServiceInfo implements Parcelable {
     @NonNull
     private final Key mKey;
diff --git a/framework-t/udc-extended-api/OWNERS b/framework-t/udc-extended-api/OWNERS
index af583c3..607f85a 100644
--- a/framework-t/udc-extended-api/OWNERS
+++ b/framework-t/udc-extended-api/OWNERS
@@ -1,2 +1,2 @@
-file:platform/packages/modules/Connectivity:master:/nearby/OWNERS
-file:platform/packages/modules/Connectivity:master:/remoteauth/OWNERS
+file:platform/packages/modules/Connectivity:main:/nearby/OWNERS
+file:platform/packages/modules/Connectivity:main:/remoteauth/OWNERS
diff --git a/framework/api/current.txt b/framework/api/current.txt
index 6860c3c..4f00977 100644
--- a/framework/api/current.txt
+++ b/framework/api/current.txt
@@ -1,4 +1,6 @@
 // Signature format: 2.0
+// - add-additional-overrides=no
+// - migrating=Migration in progress see b/299366704
 package android.net {
 
   public class CaptivePortal implements android.os.Parcelable {
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index 193bd92..ac57c10 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -1,4 +1,6 @@
 // Signature format: 2.0
+// - add-additional-overrides=no
+// - migrating=Migration in progress see b/299366704
 package android.net {
 
   public final class ConnectivityFrameworkInitializer {
diff --git a/framework/api/module-lib-removed.txt b/framework/api/module-lib-removed.txt
index d802177..14191eb 100644
--- a/framework/api/module-lib-removed.txt
+++ b/framework/api/module-lib-removed.txt
@@ -1 +1,3 @@
 // Signature format: 2.0
+// - add-additional-overrides=no
+// - migrating=Migration in progress see b/299366704
diff --git a/framework/api/removed.txt b/framework/api/removed.txt
index 303a1e6..f5da46a 100644
--- a/framework/api/removed.txt
+++ b/framework/api/removed.txt
@@ -1,4 +1,6 @@
 // Signature format: 2.0
+// - add-additional-overrides=no
+// - migrating=Migration in progress see b/299366704
 package android.net {
 
   public class ConnectivityManager {
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index 4a2ed8a..cd120e9 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -1,4 +1,6 @@
 // Signature format: 2.0
+// - add-additional-overrides=no
+// - migrating=Migration in progress see b/299366704
 package android.net {
 
   public class CaptivePortal implements android.os.Parcelable {
diff --git a/framework/api/system-removed.txt b/framework/api/system-removed.txt
index d802177..14191eb 100644
--- a/framework/api/system-removed.txt
+++ b/framework/api/system-removed.txt
@@ -1 +1,3 @@
 // Signature format: 2.0
+// - add-additional-overrides=no
+// - migrating=Migration in progress see b/299366704
diff --git a/framework/cronet_disabled/api/current.txt b/framework/cronet_disabled/api/current.txt
deleted file mode 100644
index 672e3e2..0000000
--- a/framework/cronet_disabled/api/current.txt
+++ /dev/null
@@ -1,527 +0,0 @@
-// Signature format: 2.0
-package android.net {
-
-  public class CaptivePortal implements android.os.Parcelable {
-    method public int describeContents();
-    method public void ignoreNetwork();
-    method public void reportCaptivePortalDismissed();
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.CaptivePortal> CREATOR;
-  }
-
-  public class ConnectivityDiagnosticsManager {
-    method public void registerConnectivityDiagnosticsCallback(@NonNull android.net.NetworkRequest, @NonNull java.util.concurrent.Executor, @NonNull android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback);
-    method public void unregisterConnectivityDiagnosticsCallback(@NonNull android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback);
-  }
-
-  public abstract static class ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback {
-    ctor public ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback();
-    method public void onConnectivityReportAvailable(@NonNull android.net.ConnectivityDiagnosticsManager.ConnectivityReport);
-    method public void onDataStallSuspected(@NonNull android.net.ConnectivityDiagnosticsManager.DataStallReport);
-    method public void onNetworkConnectivityReported(@NonNull android.net.Network, boolean);
-  }
-
-  public static final class ConnectivityDiagnosticsManager.ConnectivityReport implements android.os.Parcelable {
-    ctor public ConnectivityDiagnosticsManager.ConnectivityReport(@NonNull android.net.Network, long, @NonNull android.net.LinkProperties, @NonNull android.net.NetworkCapabilities, @NonNull android.os.PersistableBundle);
-    method public int describeContents();
-    method @NonNull public android.os.PersistableBundle getAdditionalInfo();
-    method @NonNull public android.net.LinkProperties getLinkProperties();
-    method @NonNull public android.net.Network getNetwork();
-    method @NonNull public android.net.NetworkCapabilities getNetworkCapabilities();
-    method public long getReportTimestamp();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.ConnectivityDiagnosticsManager.ConnectivityReport> CREATOR;
-    field public static final String KEY_NETWORK_PROBES_ATTEMPTED_BITMASK = "networkProbesAttempted";
-    field public static final String KEY_NETWORK_PROBES_SUCCEEDED_BITMASK = "networkProbesSucceeded";
-    field public static final String KEY_NETWORK_VALIDATION_RESULT = "networkValidationResult";
-    field public static final int NETWORK_PROBE_DNS = 4; // 0x4
-    field public static final int NETWORK_PROBE_FALLBACK = 32; // 0x20
-    field public static final int NETWORK_PROBE_HTTP = 8; // 0x8
-    field public static final int NETWORK_PROBE_HTTPS = 16; // 0x10
-    field public static final int NETWORK_PROBE_PRIVATE_DNS = 64; // 0x40
-    field public static final int NETWORK_VALIDATION_RESULT_INVALID = 0; // 0x0
-    field public static final int NETWORK_VALIDATION_RESULT_PARTIALLY_VALID = 2; // 0x2
-    field public static final int NETWORK_VALIDATION_RESULT_SKIPPED = 3; // 0x3
-    field public static final int NETWORK_VALIDATION_RESULT_VALID = 1; // 0x1
-  }
-
-  public static final class ConnectivityDiagnosticsManager.DataStallReport implements android.os.Parcelable {
-    ctor public ConnectivityDiagnosticsManager.DataStallReport(@NonNull android.net.Network, long, int, @NonNull android.net.LinkProperties, @NonNull android.net.NetworkCapabilities, @NonNull android.os.PersistableBundle);
-    method public int describeContents();
-    method public int getDetectionMethod();
-    method @NonNull public android.net.LinkProperties getLinkProperties();
-    method @NonNull public android.net.Network getNetwork();
-    method @NonNull public android.net.NetworkCapabilities getNetworkCapabilities();
-    method public long getReportTimestamp();
-    method @NonNull public android.os.PersistableBundle getStallDetails();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.ConnectivityDiagnosticsManager.DataStallReport> CREATOR;
-    field public static final int DETECTION_METHOD_DNS_EVENTS = 1; // 0x1
-    field public static final int DETECTION_METHOD_TCP_METRICS = 2; // 0x2
-    field public static final String KEY_DNS_CONSECUTIVE_TIMEOUTS = "dnsConsecutiveTimeouts";
-    field public static final String KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS = "tcpMetricsCollectionPeriodMillis";
-    field public static final String KEY_TCP_PACKET_FAIL_RATE = "tcpPacketFailRate";
-  }
-
-  public class ConnectivityManager {
-    method public void addDefaultNetworkActiveListener(android.net.ConnectivityManager.OnNetworkActiveListener);
-    method public boolean bindProcessToNetwork(@Nullable android.net.Network);
-    method @NonNull public android.net.SocketKeepalive createSocketKeepalive(@NonNull android.net.Network, @NonNull android.net.IpSecManager.UdpEncapsulationSocket, @NonNull java.net.InetAddress, @NonNull java.net.InetAddress, @NonNull java.util.concurrent.Executor, @NonNull android.net.SocketKeepalive.Callback);
-    method @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.Network getActiveNetwork();
-    method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.NetworkInfo getActiveNetworkInfo();
-    method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.NetworkInfo[] getAllNetworkInfo();
-    method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.Network[] getAllNetworks();
-    method @Deprecated public boolean getBackgroundDataSetting();
-    method @Nullable public android.net.Network getBoundNetworkForProcess();
-    method public int getConnectionOwnerUid(int, @NonNull java.net.InetSocketAddress, @NonNull java.net.InetSocketAddress);
-    method @Nullable public android.net.ProxyInfo getDefaultProxy();
-    method @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.LinkProperties getLinkProperties(@Nullable android.net.Network);
-    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public int getMultipathPreference(@Nullable android.net.Network);
-    method @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.NetworkCapabilities getNetworkCapabilities(@Nullable android.net.Network);
-    method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.NetworkInfo getNetworkInfo(int);
-    method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.NetworkInfo getNetworkInfo(@Nullable android.net.Network);
-    method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public int getNetworkPreference();
-    method @Nullable public byte[] getNetworkWatchlistConfigHash();
-    method @Deprecated @Nullable public static android.net.Network getProcessDefaultNetwork();
-    method public int getRestrictBackgroundStatus();
-    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public boolean isActiveNetworkMetered();
-    method public boolean isDefaultNetworkActive();
-    method @Deprecated public static boolean isNetworkTypeValid(int);
-    method public void registerBestMatchingNetworkCallback(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
-    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerDefaultNetworkCallback(@NonNull android.net.ConnectivityManager.NetworkCallback);
-    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerDefaultNetworkCallback(@NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
-    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerNetworkCallback(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback);
-    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerNetworkCallback(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
-    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerNetworkCallback(@NonNull android.net.NetworkRequest, @NonNull android.app.PendingIntent);
-    method public void releaseNetworkRequest(@NonNull android.app.PendingIntent);
-    method public void removeDefaultNetworkActiveListener(@NonNull android.net.ConnectivityManager.OnNetworkActiveListener);
-    method @Deprecated public void reportBadNetwork(@Nullable android.net.Network);
-    method public void reportNetworkConnectivity(@Nullable android.net.Network, boolean);
-    method public boolean requestBandwidthUpdate(@NonNull android.net.Network);
-    method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback);
-    method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
-    method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, int);
-    method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler, int);
-    method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.app.PendingIntent);
-    method @Deprecated public void setNetworkPreference(int);
-    method @Deprecated public static boolean setProcessDefaultNetwork(@Nullable android.net.Network);
-    method public void unregisterNetworkCallback(@NonNull android.net.ConnectivityManager.NetworkCallback);
-    method public void unregisterNetworkCallback(@NonNull android.app.PendingIntent);
-    field @Deprecated public static final String ACTION_BACKGROUND_DATA_SETTING_CHANGED = "android.net.conn.BACKGROUND_DATA_SETTING_CHANGED";
-    field public static final String ACTION_CAPTIVE_PORTAL_SIGN_IN = "android.net.conn.CAPTIVE_PORTAL";
-    field public static final String ACTION_RESTRICT_BACKGROUND_CHANGED = "android.net.conn.RESTRICT_BACKGROUND_CHANGED";
-    field @Deprecated public static final String CONNECTIVITY_ACTION = "android.net.conn.CONNECTIVITY_CHANGE";
-    field @Deprecated public static final int DEFAULT_NETWORK_PREFERENCE = 1; // 0x1
-    field public static final String EXTRA_CAPTIVE_PORTAL = "android.net.extra.CAPTIVE_PORTAL";
-    field public static final String EXTRA_CAPTIVE_PORTAL_URL = "android.net.extra.CAPTIVE_PORTAL_URL";
-    field @Deprecated public static final String EXTRA_EXTRA_INFO = "extraInfo";
-    field @Deprecated public static final String EXTRA_IS_FAILOVER = "isFailover";
-    field public static final String EXTRA_NETWORK = "android.net.extra.NETWORK";
-    field @Deprecated public static final String EXTRA_NETWORK_INFO = "networkInfo";
-    field public static final String EXTRA_NETWORK_REQUEST = "android.net.extra.NETWORK_REQUEST";
-    field @Deprecated public static final String EXTRA_NETWORK_TYPE = "networkType";
-    field public static final String EXTRA_NO_CONNECTIVITY = "noConnectivity";
-    field @Deprecated public static final String EXTRA_OTHER_NETWORK_INFO = "otherNetwork";
-    field public static final String EXTRA_REASON = "reason";
-    field public static final int MULTIPATH_PREFERENCE_HANDOVER = 1; // 0x1
-    field public static final int MULTIPATH_PREFERENCE_PERFORMANCE = 4; // 0x4
-    field public static final int MULTIPATH_PREFERENCE_RELIABILITY = 2; // 0x2
-    field public static final int RESTRICT_BACKGROUND_STATUS_DISABLED = 1; // 0x1
-    field public static final int RESTRICT_BACKGROUND_STATUS_ENABLED = 3; // 0x3
-    field public static final int RESTRICT_BACKGROUND_STATUS_WHITELISTED = 2; // 0x2
-    field @Deprecated public static final int TYPE_BLUETOOTH = 7; // 0x7
-    field @Deprecated public static final int TYPE_DUMMY = 8; // 0x8
-    field @Deprecated public static final int TYPE_ETHERNET = 9; // 0x9
-    field @Deprecated public static final int TYPE_MOBILE = 0; // 0x0
-    field @Deprecated public static final int TYPE_MOBILE_DUN = 4; // 0x4
-    field @Deprecated public static final int TYPE_MOBILE_HIPRI = 5; // 0x5
-    field @Deprecated public static final int TYPE_MOBILE_MMS = 2; // 0x2
-    field @Deprecated public static final int TYPE_MOBILE_SUPL = 3; // 0x3
-    field @Deprecated public static final int TYPE_VPN = 17; // 0x11
-    field @Deprecated public static final int TYPE_WIFI = 1; // 0x1
-    field @Deprecated public static final int TYPE_WIMAX = 6; // 0x6
-  }
-
-  public static class ConnectivityManager.NetworkCallback {
-    ctor public ConnectivityManager.NetworkCallback();
-    ctor public ConnectivityManager.NetworkCallback(int);
-    method public void onAvailable(@NonNull android.net.Network);
-    method public void onBlockedStatusChanged(@NonNull android.net.Network, boolean);
-    method public void onCapabilitiesChanged(@NonNull android.net.Network, @NonNull android.net.NetworkCapabilities);
-    method public void onLinkPropertiesChanged(@NonNull android.net.Network, @NonNull android.net.LinkProperties);
-    method public void onLosing(@NonNull android.net.Network, int);
-    method public void onLost(@NonNull android.net.Network);
-    method public void onUnavailable();
-    field public static final int FLAG_INCLUDE_LOCATION_INFO = 1; // 0x1
-  }
-
-  public static interface ConnectivityManager.OnNetworkActiveListener {
-    method public void onNetworkActive();
-  }
-
-  public class DhcpInfo implements android.os.Parcelable {
-    ctor public DhcpInfo();
-    method public int describeContents();
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.DhcpInfo> CREATOR;
-    field public int dns1;
-    field public int dns2;
-    field public int gateway;
-    field public int ipAddress;
-    field public int leaseDuration;
-    field public int netmask;
-    field public int serverAddress;
-  }
-
-  public final class DnsResolver {
-    method @NonNull public static android.net.DnsResolver getInstance();
-    method public void query(@Nullable android.net.Network, @NonNull String, int, @NonNull java.util.concurrent.Executor, @Nullable android.os.CancellationSignal, @NonNull android.net.DnsResolver.Callback<? super java.util.List<java.net.InetAddress>>);
-    method public void query(@Nullable android.net.Network, @NonNull String, int, int, @NonNull java.util.concurrent.Executor, @Nullable android.os.CancellationSignal, @NonNull android.net.DnsResolver.Callback<? super java.util.List<java.net.InetAddress>>);
-    method public void rawQuery(@Nullable android.net.Network, @NonNull byte[], int, @NonNull java.util.concurrent.Executor, @Nullable android.os.CancellationSignal, @NonNull android.net.DnsResolver.Callback<? super byte[]>);
-    method public void rawQuery(@Nullable android.net.Network, @NonNull String, int, int, int, @NonNull java.util.concurrent.Executor, @Nullable android.os.CancellationSignal, @NonNull android.net.DnsResolver.Callback<? super byte[]>);
-    field public static final int CLASS_IN = 1; // 0x1
-    field public static final int ERROR_PARSE = 0; // 0x0
-    field public static final int ERROR_SYSTEM = 1; // 0x1
-    field public static final int FLAG_EMPTY = 0; // 0x0
-    field public static final int FLAG_NO_CACHE_LOOKUP = 4; // 0x4
-    field public static final int FLAG_NO_CACHE_STORE = 2; // 0x2
-    field public static final int FLAG_NO_RETRY = 1; // 0x1
-    field public static final int TYPE_A = 1; // 0x1
-    field public static final int TYPE_AAAA = 28; // 0x1c
-  }
-
-  public static interface DnsResolver.Callback<T> {
-    method public void onAnswer(@NonNull T, int);
-    method public void onError(@NonNull android.net.DnsResolver.DnsException);
-  }
-
-  public static class DnsResolver.DnsException extends java.lang.Exception {
-    ctor public DnsResolver.DnsException(int, @Nullable Throwable);
-    field public final int code;
-  }
-
-  public class InetAddresses {
-    method public static boolean isNumericAddress(@NonNull String);
-    method @NonNull public static java.net.InetAddress parseNumericAddress(@NonNull String);
-  }
-
-  public final class IpConfiguration implements android.os.Parcelable {
-    method public int describeContents();
-    method @Nullable public android.net.ProxyInfo getHttpProxy();
-    method @Nullable public android.net.StaticIpConfiguration getStaticIpConfiguration();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.IpConfiguration> CREATOR;
-  }
-
-  public static final class IpConfiguration.Builder {
-    ctor public IpConfiguration.Builder();
-    method @NonNull public android.net.IpConfiguration build();
-    method @NonNull public android.net.IpConfiguration.Builder setHttpProxy(@Nullable android.net.ProxyInfo);
-    method @NonNull public android.net.IpConfiguration.Builder setStaticIpConfiguration(@Nullable android.net.StaticIpConfiguration);
-  }
-
-  public final class IpPrefix implements android.os.Parcelable {
-    ctor public IpPrefix(@NonNull java.net.InetAddress, @IntRange(from=0, to=128) int);
-    method public boolean contains(@NonNull java.net.InetAddress);
-    method public int describeContents();
-    method @NonNull public java.net.InetAddress getAddress();
-    method @IntRange(from=0, to=128) public int getPrefixLength();
-    method @NonNull public byte[] getRawAddress();
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.IpPrefix> CREATOR;
-  }
-
-  public class LinkAddress implements android.os.Parcelable {
-    method public int describeContents();
-    method public java.net.InetAddress getAddress();
-    method public int getFlags();
-    method @IntRange(from=0, to=128) public int getPrefixLength();
-    method public int getScope();
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.LinkAddress> CREATOR;
-  }
-
-  public final class LinkProperties implements android.os.Parcelable {
-    ctor public LinkProperties();
-    method public boolean addRoute(@NonNull android.net.RouteInfo);
-    method public void clear();
-    method public int describeContents();
-    method @Nullable public java.net.Inet4Address getDhcpServerAddress();
-    method @NonNull public java.util.List<java.net.InetAddress> getDnsServers();
-    method @Nullable public String getDomains();
-    method @Nullable public android.net.ProxyInfo getHttpProxy();
-    method @Nullable public String getInterfaceName();
-    method @NonNull public java.util.List<android.net.LinkAddress> getLinkAddresses();
-    method public int getMtu();
-    method @Nullable public android.net.IpPrefix getNat64Prefix();
-    method @Nullable public String getPrivateDnsServerName();
-    method @NonNull public java.util.List<android.net.RouteInfo> getRoutes();
-    method public boolean isPrivateDnsActive();
-    method public boolean isWakeOnLanSupported();
-    method public void setDhcpServerAddress(@Nullable java.net.Inet4Address);
-    method public void setDnsServers(@NonNull java.util.Collection<java.net.InetAddress>);
-    method public void setDomains(@Nullable String);
-    method public void setHttpProxy(@Nullable android.net.ProxyInfo);
-    method public void setInterfaceName(@Nullable String);
-    method public void setLinkAddresses(@NonNull java.util.Collection<android.net.LinkAddress>);
-    method public void setMtu(int);
-    method public void setNat64Prefix(@Nullable android.net.IpPrefix);
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.LinkProperties> CREATOR;
-  }
-
-  public final class MacAddress implements android.os.Parcelable {
-    method public int describeContents();
-    method @NonNull public static android.net.MacAddress fromBytes(@NonNull byte[]);
-    method @NonNull public static android.net.MacAddress fromString(@NonNull String);
-    method public int getAddressType();
-    method @Nullable public java.net.Inet6Address getLinkLocalIpv6FromEui48Mac();
-    method public boolean isLocallyAssigned();
-    method public boolean matches(@NonNull android.net.MacAddress, @NonNull android.net.MacAddress);
-    method @NonNull public byte[] toByteArray();
-    method @NonNull public String toOuiString();
-    method public void writeToParcel(android.os.Parcel, int);
-    field public static final android.net.MacAddress BROADCAST_ADDRESS;
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.MacAddress> CREATOR;
-    field public static final int TYPE_BROADCAST = 3; // 0x3
-    field public static final int TYPE_MULTICAST = 2; // 0x2
-    field public static final int TYPE_UNICAST = 1; // 0x1
-  }
-
-  public class Network implements android.os.Parcelable {
-    method public void bindSocket(java.net.DatagramSocket) throws java.io.IOException;
-    method public void bindSocket(java.net.Socket) throws java.io.IOException;
-    method public void bindSocket(java.io.FileDescriptor) throws java.io.IOException;
-    method public int describeContents();
-    method public static android.net.Network fromNetworkHandle(long);
-    method public java.net.InetAddress[] getAllByName(String) throws java.net.UnknownHostException;
-    method public java.net.InetAddress getByName(String) throws java.net.UnknownHostException;
-    method public long getNetworkHandle();
-    method public javax.net.SocketFactory getSocketFactory();
-    method public java.net.URLConnection openConnection(java.net.URL) throws java.io.IOException;
-    method public java.net.URLConnection openConnection(java.net.URL, java.net.Proxy) throws java.io.IOException;
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.Network> CREATOR;
-  }
-
-  public final class NetworkCapabilities implements android.os.Parcelable {
-    ctor public NetworkCapabilities();
-    ctor public NetworkCapabilities(android.net.NetworkCapabilities);
-    method public int describeContents();
-    method @NonNull public int[] getCapabilities();
-    method @NonNull public int[] getEnterpriseIds();
-    method public int getLinkDownstreamBandwidthKbps();
-    method public int getLinkUpstreamBandwidthKbps();
-    method @Nullable public android.net.NetworkSpecifier getNetworkSpecifier();
-    method public int getOwnerUid();
-    method public int getSignalStrength();
-    method @Nullable public android.net.TransportInfo getTransportInfo();
-    method public boolean hasCapability(int);
-    method public boolean hasEnterpriseId(int);
-    method public boolean hasTransport(int);
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkCapabilities> CREATOR;
-    field public static final int NET_CAPABILITY_CAPTIVE_PORTAL = 17; // 0x11
-    field public static final int NET_CAPABILITY_CBS = 5; // 0x5
-    field public static final int NET_CAPABILITY_DUN = 2; // 0x2
-    field public static final int NET_CAPABILITY_EIMS = 10; // 0xa
-    field public static final int NET_CAPABILITY_ENTERPRISE = 29; // 0x1d
-    field public static final int NET_CAPABILITY_FOREGROUND = 19; // 0x13
-    field public static final int NET_CAPABILITY_FOTA = 3; // 0x3
-    field public static final int NET_CAPABILITY_HEAD_UNIT = 32; // 0x20
-    field public static final int NET_CAPABILITY_IA = 7; // 0x7
-    field public static final int NET_CAPABILITY_IMS = 4; // 0x4
-    field public static final int NET_CAPABILITY_INTERNET = 12; // 0xc
-    field public static final int NET_CAPABILITY_MCX = 23; // 0x17
-    field public static final int NET_CAPABILITY_MMS = 0; // 0x0
-    field public static final int NET_CAPABILITY_MMTEL = 33; // 0x21
-    field public static final int NET_CAPABILITY_NOT_CONGESTED = 20; // 0x14
-    field public static final int NET_CAPABILITY_NOT_METERED = 11; // 0xb
-    field public static final int NET_CAPABILITY_NOT_RESTRICTED = 13; // 0xd
-    field public static final int NET_CAPABILITY_NOT_ROAMING = 18; // 0x12
-    field public static final int NET_CAPABILITY_NOT_SUSPENDED = 21; // 0x15
-    field public static final int NET_CAPABILITY_NOT_VPN = 15; // 0xf
-    field public static final int NET_CAPABILITY_PRIORITIZE_BANDWIDTH = 35; // 0x23
-    field public static final int NET_CAPABILITY_PRIORITIZE_LATENCY = 34; // 0x22
-    field public static final int NET_CAPABILITY_RCS = 8; // 0x8
-    field public static final int NET_CAPABILITY_SUPL = 1; // 0x1
-    field public static final int NET_CAPABILITY_TEMPORARILY_NOT_METERED = 25; // 0x19
-    field public static final int NET_CAPABILITY_TRUSTED = 14; // 0xe
-    field public static final int NET_CAPABILITY_VALIDATED = 16; // 0x10
-    field public static final int NET_CAPABILITY_WIFI_P2P = 6; // 0x6
-    field public static final int NET_CAPABILITY_XCAP = 9; // 0x9
-    field public static final int NET_ENTERPRISE_ID_1 = 1; // 0x1
-    field public static final int NET_ENTERPRISE_ID_2 = 2; // 0x2
-    field public static final int NET_ENTERPRISE_ID_3 = 3; // 0x3
-    field public static final int NET_ENTERPRISE_ID_4 = 4; // 0x4
-    field public static final int NET_ENTERPRISE_ID_5 = 5; // 0x5
-    field public static final int SIGNAL_STRENGTH_UNSPECIFIED = -2147483648; // 0x80000000
-    field public static final int TRANSPORT_BLUETOOTH = 2; // 0x2
-    field public static final int TRANSPORT_CELLULAR = 0; // 0x0
-    field public static final int TRANSPORT_ETHERNET = 3; // 0x3
-    field public static final int TRANSPORT_LOWPAN = 6; // 0x6
-    field public static final int TRANSPORT_THREAD = 9; // 0x9
-    field public static final int TRANSPORT_USB = 8; // 0x8
-    field public static final int TRANSPORT_VPN = 4; // 0x4
-    field public static final int TRANSPORT_WIFI = 1; // 0x1
-    field public static final int TRANSPORT_WIFI_AWARE = 5; // 0x5
-  }
-
-  @Deprecated public class NetworkInfo implements android.os.Parcelable {
-    ctor @Deprecated public NetworkInfo(int, int, @Nullable String, @Nullable String);
-    method @Deprecated public int describeContents();
-    method @Deprecated @NonNull public android.net.NetworkInfo.DetailedState getDetailedState();
-    method @Deprecated public String getExtraInfo();
-    method @Deprecated public String getReason();
-    method @Deprecated public android.net.NetworkInfo.State getState();
-    method @Deprecated public int getSubtype();
-    method @Deprecated public String getSubtypeName();
-    method @Deprecated public int getType();
-    method @Deprecated public String getTypeName();
-    method @Deprecated public boolean isAvailable();
-    method @Deprecated public boolean isConnected();
-    method @Deprecated public boolean isConnectedOrConnecting();
-    method @Deprecated public boolean isFailover();
-    method @Deprecated public boolean isRoaming();
-    method @Deprecated public void setDetailedState(@NonNull android.net.NetworkInfo.DetailedState, @Nullable String, @Nullable String);
-    method @Deprecated public void writeToParcel(android.os.Parcel, int);
-    field @Deprecated @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkInfo> CREATOR;
-  }
-
-  @Deprecated public enum NetworkInfo.DetailedState {
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState AUTHENTICATING;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState BLOCKED;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState CAPTIVE_PORTAL_CHECK;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState CONNECTED;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState CONNECTING;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState DISCONNECTED;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState DISCONNECTING;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState FAILED;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState IDLE;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState OBTAINING_IPADDR;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState SCANNING;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState SUSPENDED;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState VERIFYING_POOR_LINK;
-  }
-
-  @Deprecated public enum NetworkInfo.State {
-    enum_constant @Deprecated public static final android.net.NetworkInfo.State CONNECTED;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.State CONNECTING;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.State DISCONNECTED;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.State DISCONNECTING;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.State SUSPENDED;
-    enum_constant @Deprecated public static final android.net.NetworkInfo.State UNKNOWN;
-  }
-
-  public class NetworkRequest implements android.os.Parcelable {
-    method public boolean canBeSatisfiedBy(@Nullable android.net.NetworkCapabilities);
-    method public int describeContents();
-    method @NonNull public int[] getCapabilities();
-    method @Nullable public android.net.NetworkSpecifier getNetworkSpecifier();
-    method @NonNull public int[] getTransportTypes();
-    method public boolean hasCapability(int);
-    method public boolean hasTransport(int);
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkRequest> CREATOR;
-  }
-
-  public static class NetworkRequest.Builder {
-    ctor public NetworkRequest.Builder();
-    ctor public NetworkRequest.Builder(@NonNull android.net.NetworkRequest);
-    method public android.net.NetworkRequest.Builder addCapability(int);
-    method public android.net.NetworkRequest.Builder addTransportType(int);
-    method public android.net.NetworkRequest build();
-    method @NonNull public android.net.NetworkRequest.Builder clearCapabilities();
-    method public android.net.NetworkRequest.Builder removeCapability(int);
-    method public android.net.NetworkRequest.Builder removeTransportType(int);
-    method @NonNull public android.net.NetworkRequest.Builder setIncludeOtherUidNetworks(boolean);
-    method @Deprecated public android.net.NetworkRequest.Builder setNetworkSpecifier(String);
-    method public android.net.NetworkRequest.Builder setNetworkSpecifier(android.net.NetworkSpecifier);
-  }
-
-  public class ParseException extends java.lang.RuntimeException {
-    ctor public ParseException(@NonNull String);
-    ctor public ParseException(@NonNull String, @NonNull Throwable);
-    field public String response;
-  }
-
-  public class ProxyInfo implements android.os.Parcelable {
-    ctor public ProxyInfo(@Nullable android.net.ProxyInfo);
-    method public static android.net.ProxyInfo buildDirectProxy(String, int);
-    method public static android.net.ProxyInfo buildDirectProxy(String, int, java.util.List<java.lang.String>);
-    method public static android.net.ProxyInfo buildPacProxy(android.net.Uri);
-    method @NonNull public static android.net.ProxyInfo buildPacProxy(@NonNull android.net.Uri, int);
-    method public int describeContents();
-    method public String[] getExclusionList();
-    method public String getHost();
-    method public android.net.Uri getPacFileUrl();
-    method public int getPort();
-    method public boolean isValid();
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.ProxyInfo> CREATOR;
-  }
-
-  public final class RouteInfo implements android.os.Parcelable {
-    method public int describeContents();
-    method @NonNull public android.net.IpPrefix getDestination();
-    method @Nullable public java.net.InetAddress getGateway();
-    method @Nullable public String getInterface();
-    method public int getType();
-    method public boolean hasGateway();
-    method public boolean isDefaultRoute();
-    method public boolean matches(java.net.InetAddress);
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.RouteInfo> CREATOR;
-    field public static final int RTN_THROW = 9; // 0x9
-    field public static final int RTN_UNICAST = 1; // 0x1
-    field public static final int RTN_UNREACHABLE = 7; // 0x7
-  }
-
-  public abstract class SocketKeepalive implements java.lang.AutoCloseable {
-    method public final void close();
-    method public final void start(@IntRange(from=0xa, to=0xe10) int);
-    method public final void stop();
-    field public static final int ERROR_HARDWARE_ERROR = -31; // 0xffffffe1
-    field public static final int ERROR_INSUFFICIENT_RESOURCES = -32; // 0xffffffe0
-    field public static final int ERROR_INVALID_INTERVAL = -24; // 0xffffffe8
-    field public static final int ERROR_INVALID_IP_ADDRESS = -21; // 0xffffffeb
-    field public static final int ERROR_INVALID_LENGTH = -23; // 0xffffffe9
-    field public static final int ERROR_INVALID_NETWORK = -20; // 0xffffffec
-    field public static final int ERROR_INVALID_PORT = -22; // 0xffffffea
-    field public static final int ERROR_INVALID_SOCKET = -25; // 0xffffffe7
-    field public static final int ERROR_SOCKET_NOT_IDLE = -26; // 0xffffffe6
-    field public static final int ERROR_UNSUPPORTED = -30; // 0xffffffe2
-  }
-
-  public static class SocketKeepalive.Callback {
-    ctor public SocketKeepalive.Callback();
-    method public void onDataReceived();
-    method public void onError(int);
-    method public void onStarted();
-    method public void onStopped();
-  }
-
-  public final class StaticIpConfiguration implements android.os.Parcelable {
-    method public int describeContents();
-    method @NonNull public java.util.List<java.net.InetAddress> getDnsServers();
-    method @Nullable public String getDomains();
-    method @Nullable public java.net.InetAddress getGateway();
-    method @NonNull public android.net.LinkAddress getIpAddress();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.StaticIpConfiguration> CREATOR;
-  }
-
-  public static final class StaticIpConfiguration.Builder {
-    ctor public StaticIpConfiguration.Builder();
-    method @NonNull public android.net.StaticIpConfiguration build();
-    method @NonNull public android.net.StaticIpConfiguration.Builder setDnsServers(@NonNull Iterable<java.net.InetAddress>);
-    method @NonNull public android.net.StaticIpConfiguration.Builder setDomains(@Nullable String);
-    method @NonNull public android.net.StaticIpConfiguration.Builder setGateway(@Nullable java.net.InetAddress);
-    method @NonNull public android.net.StaticIpConfiguration.Builder setIpAddress(@NonNull android.net.LinkAddress);
-  }
-
-  public interface TransportInfo {
-  }
-
-}
-
diff --git a/framework/cronet_disabled/api/lint-baseline.txt b/framework/cronet_disabled/api/lint-baseline.txt
deleted file mode 100644
index 2f4004a..0000000
--- a/framework/cronet_disabled/api/lint-baseline.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-// Baseline format: 1.0
-VisiblySynchronized: android.net.NetworkInfo#toString():
-    Internal locks must not be exposed (synchronizing on this or class is still
-    externally observable): method android.net.NetworkInfo.toString()
diff --git a/framework/cronet_disabled/api/module-lib-current.txt b/framework/cronet_disabled/api/module-lib-current.txt
deleted file mode 100644
index 193bd92..0000000
--- a/framework/cronet_disabled/api/module-lib-current.txt
+++ /dev/null
@@ -1,239 +0,0 @@
-// Signature format: 2.0
-package android.net {
-
-  public final class ConnectivityFrameworkInitializer {
-    method public static void registerServiceWrappers();
-  }
-
-  public class ConnectivityManager {
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void addUidToMeteredNetworkAllowList(int);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void addUidToMeteredNetworkDenyList(int);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void factoryReset();
-    method @NonNull @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public java.util.List<android.net.NetworkStateSnapshot> getAllNetworkStateSnapshots();
-    method @Nullable public android.net.ProxyInfo getGlobalProxy();
-    method @NonNull public static android.util.Range<java.lang.Integer> getIpSecNetIdRange();
-    method @Nullable @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public android.net.LinkProperties getRedactedLinkPropertiesForPackage(@NonNull android.net.LinkProperties, int, @NonNull String);
-    method @Nullable @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public android.net.NetworkCapabilities getRedactedNetworkCapabilitiesForPackage(@NonNull android.net.NetworkCapabilities, int, @NonNull String);
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void registerDefaultNetworkCallbackForUid(int, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS}) public void registerSystemDefaultNetworkCallback(@NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void removeUidFromMeteredNetworkAllowList(int);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void removeUidFromMeteredNetworkDenyList(int);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void replaceFirewallChain(int, @NonNull int[]);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void requestBackgroundNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
-    method @Deprecated public boolean requestRouteToHostAddress(int, java.net.InetAddress);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setAcceptPartialConnectivity(@NonNull android.net.Network, boolean, boolean);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setAcceptUnvalidated(@NonNull android.net.Network, boolean, boolean);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setAvoidUnvalidated(@NonNull android.net.Network);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setFirewallChainEnabled(int, boolean);
-    method @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public void setGlobalProxy(@Nullable android.net.ProxyInfo);
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void setLegacyLockdownVpnEnabled(boolean);
-    method @Deprecated @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public void setProfileNetworkPreference(@NonNull android.os.UserHandle, int, @Nullable java.util.concurrent.Executor, @Nullable Runnable);
-    method @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public void setProfileNetworkPreferences(@NonNull android.os.UserHandle, @NonNull java.util.List<android.net.ProfileNetworkPreference>, @Nullable java.util.concurrent.Executor, @Nullable Runnable);
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void setRequireVpnForUids(boolean, @NonNull java.util.Collection<android.util.Range<java.lang.Integer>>);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setUidFirewallRule(int, int, int);
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void setVpnDefaultForUids(@NonNull String, @NonNull java.util.Collection<android.util.Range<java.lang.Integer>>);
-    method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_TEST_NETWORKS, android.Manifest.permission.NETWORK_STACK}) public void simulateDataStall(int, long, @NonNull android.net.Network, @NonNull android.os.PersistableBundle);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void startCaptivePortalApp(@NonNull android.net.Network);
-    method public void systemReady();
-    field public static final String ACTION_CLEAR_DNS_CACHE = "android.net.action.CLEAR_DNS_CACHE";
-    field public static final String ACTION_PROMPT_LOST_VALIDATION = "android.net.action.PROMPT_LOST_VALIDATION";
-    field public static final String ACTION_PROMPT_PARTIAL_CONNECTIVITY = "android.net.action.PROMPT_PARTIAL_CONNECTIVITY";
-    field public static final String ACTION_PROMPT_UNVALIDATED = "android.net.action.PROMPT_UNVALIDATED";
-    field public static final int BLOCKED_METERED_REASON_ADMIN_DISABLED = 262144; // 0x40000
-    field public static final int BLOCKED_METERED_REASON_DATA_SAVER = 65536; // 0x10000
-    field public static final int BLOCKED_METERED_REASON_MASK = -65536; // 0xffff0000
-    field public static final int BLOCKED_METERED_REASON_USER_RESTRICTED = 131072; // 0x20000
-    field public static final int BLOCKED_REASON_APP_STANDBY = 4; // 0x4
-    field public static final int BLOCKED_REASON_BATTERY_SAVER = 1; // 0x1
-    field public static final int BLOCKED_REASON_DOZE = 2; // 0x2
-    field public static final int BLOCKED_REASON_LOCKDOWN_VPN = 16; // 0x10
-    field public static final int BLOCKED_REASON_LOW_POWER_STANDBY = 32; // 0x20
-    field public static final int BLOCKED_REASON_NONE = 0; // 0x0
-    field public static final int BLOCKED_REASON_RESTRICTED_MODE = 8; // 0x8
-    field public static final int FIREWALL_CHAIN_DOZABLE = 1; // 0x1
-    field public static final int FIREWALL_CHAIN_LOW_POWER_STANDBY = 5; // 0x5
-    field public static final int FIREWALL_CHAIN_OEM_DENY_1 = 7; // 0x7
-    field public static final int FIREWALL_CHAIN_OEM_DENY_2 = 8; // 0x8
-    field public static final int FIREWALL_CHAIN_OEM_DENY_3 = 9; // 0x9
-    field public static final int FIREWALL_CHAIN_POWERSAVE = 3; // 0x3
-    field public static final int FIREWALL_CHAIN_RESTRICTED = 4; // 0x4
-    field public static final int FIREWALL_CHAIN_STANDBY = 2; // 0x2
-    field public static final int FIREWALL_RULE_ALLOW = 1; // 0x1
-    field public static final int FIREWALL_RULE_DEFAULT = 0; // 0x0
-    field public static final int FIREWALL_RULE_DENY = 2; // 0x2
-    field public static final int PROFILE_NETWORK_PREFERENCE_DEFAULT = 0; // 0x0
-    field public static final int PROFILE_NETWORK_PREFERENCE_ENTERPRISE = 1; // 0x1
-    field public static final int PROFILE_NETWORK_PREFERENCE_ENTERPRISE_BLOCKING = 3; // 0x3
-    field public static final int PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK = 2; // 0x2
-  }
-
-  public static class ConnectivityManager.NetworkCallback {
-    method public void onBlockedStatusChanged(@NonNull android.net.Network, int);
-  }
-
-  public class ConnectivitySettingsManager {
-    method public static void clearGlobalProxy(@NonNull android.content.Context);
-    method @Nullable public static String getCaptivePortalHttpUrl(@NonNull android.content.Context);
-    method public static int getCaptivePortalMode(@NonNull android.content.Context, int);
-    method @NonNull public static java.time.Duration getConnectivityKeepPendingIntentDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
-    method @NonNull public static android.util.Range<java.lang.Integer> getDnsResolverSampleRanges(@NonNull android.content.Context);
-    method @NonNull public static java.time.Duration getDnsResolverSampleValidityDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
-    method public static int getDnsResolverSuccessThresholdPercent(@NonNull android.content.Context, int);
-    method @Nullable public static android.net.ProxyInfo getGlobalProxy(@NonNull android.content.Context);
-    method public static long getIngressRateLimitInBytesPerSecond(@NonNull android.content.Context);
-    method @NonNull public static java.time.Duration getMobileDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration);
-    method public static boolean getMobileDataAlwaysOn(@NonNull android.content.Context, boolean);
-    method @NonNull public static java.util.Set<java.lang.Integer> getMobileDataPreferredUids(@NonNull android.content.Context);
-    method public static int getNetworkAvoidBadWifi(@NonNull android.content.Context);
-    method @Nullable public static String getNetworkMeteredMultipathPreference(@NonNull android.content.Context);
-    method public static int getNetworkSwitchNotificationMaximumDailyCount(@NonNull android.content.Context, int);
-    method @NonNull public static java.time.Duration getNetworkSwitchNotificationRateDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
-    method @NonNull public static String getPrivateDnsDefaultMode(@NonNull android.content.Context);
-    method @Nullable public static String getPrivateDnsHostname(@NonNull android.content.Context);
-    method public static int getPrivateDnsMode(@NonNull android.content.Context);
-    method @NonNull public static java.util.Set<java.lang.Integer> getUidsAllowedOnRestrictedNetworks(@NonNull android.content.Context);
-    method public static boolean getWifiAlwaysRequested(@NonNull android.content.Context, boolean);
-    method @NonNull public static java.time.Duration getWifiDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration);
-    method public static void setCaptivePortalHttpUrl(@NonNull android.content.Context, @Nullable String);
-    method public static void setCaptivePortalMode(@NonNull android.content.Context, int);
-    method public static void setConnectivityKeepPendingIntentDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
-    method public static void setDnsResolverSampleRanges(@NonNull android.content.Context, @NonNull android.util.Range<java.lang.Integer>);
-    method public static void setDnsResolverSampleValidityDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
-    method public static void setDnsResolverSuccessThresholdPercent(@NonNull android.content.Context, @IntRange(from=0, to=100) int);
-    method public static void setGlobalProxy(@NonNull android.content.Context, @NonNull android.net.ProxyInfo);
-    method public static void setIngressRateLimitInBytesPerSecond(@NonNull android.content.Context, @IntRange(from=-1L, to=4294967295L) long);
-    method public static void setMobileDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration);
-    method public static void setMobileDataAlwaysOn(@NonNull android.content.Context, boolean);
-    method public static void setMobileDataPreferredUids(@NonNull android.content.Context, @NonNull java.util.Set<java.lang.Integer>);
-    method public static void setNetworkAvoidBadWifi(@NonNull android.content.Context, int);
-    method public static void setNetworkMeteredMultipathPreference(@NonNull android.content.Context, @NonNull String);
-    method public static void setNetworkSwitchNotificationMaximumDailyCount(@NonNull android.content.Context, @IntRange(from=0) int);
-    method public static void setNetworkSwitchNotificationRateDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
-    method public static void setPrivateDnsDefaultMode(@NonNull android.content.Context, @NonNull int);
-    method public static void setPrivateDnsHostname(@NonNull android.content.Context, @Nullable String);
-    method public static void setPrivateDnsMode(@NonNull android.content.Context, int);
-    method public static void setUidsAllowedOnRestrictedNetworks(@NonNull android.content.Context, @NonNull java.util.Set<java.lang.Integer>);
-    method public static void setWifiAlwaysRequested(@NonNull android.content.Context, boolean);
-    method public static void setWifiDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration);
-    field public static final int CAPTIVE_PORTAL_MODE_AVOID = 2; // 0x2
-    field public static final int CAPTIVE_PORTAL_MODE_IGNORE = 0; // 0x0
-    field public static final int CAPTIVE_PORTAL_MODE_PROMPT = 1; // 0x1
-    field public static final int NETWORK_AVOID_BAD_WIFI_AVOID = 2; // 0x2
-    field public static final int NETWORK_AVOID_BAD_WIFI_IGNORE = 0; // 0x0
-    field public static final int NETWORK_AVOID_BAD_WIFI_PROMPT = 1; // 0x1
-    field public static final int PRIVATE_DNS_MODE_OFF = 1; // 0x1
-    field public static final int PRIVATE_DNS_MODE_OPPORTUNISTIC = 2; // 0x2
-    field public static final int PRIVATE_DNS_MODE_PROVIDER_HOSTNAME = 3; // 0x3
-  }
-
-  public final class DhcpOption implements android.os.Parcelable {
-    ctor public DhcpOption(byte, @Nullable byte[]);
-    method public int describeContents();
-    method public byte getType();
-    method @Nullable public byte[] getValue();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.DhcpOption> CREATOR;
-  }
-
-  public final class NetworkAgentConfig implements android.os.Parcelable {
-    method @Nullable public String getSubscriberId();
-    method public boolean isBypassableVpn();
-    method public boolean isVpnValidationRequired();
-  }
-
-  public static final class NetworkAgentConfig.Builder {
-    method @NonNull public android.net.NetworkAgentConfig.Builder setBypassableVpn(boolean);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setLocalRoutesExcludedForVpn(boolean);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setSubscriberId(@Nullable String);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setVpnRequiresValidation(boolean);
-  }
-
-  public final class NetworkCapabilities implements android.os.Parcelable {
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public java.util.Set<java.lang.Integer> getAllowedUids();
-    method @Nullable public java.util.Set<android.util.Range<java.lang.Integer>> getUids();
-    method public boolean hasForbiddenCapability(int);
-    field public static final long REDACT_ALL = -1L; // 0xffffffffffffffffL
-    field public static final long REDACT_FOR_ACCESS_FINE_LOCATION = 1L; // 0x1L
-    field public static final long REDACT_FOR_LOCAL_MAC_ADDRESS = 2L; // 0x2L
-    field public static final long REDACT_FOR_NETWORK_SETTINGS = 4L; // 0x4L
-    field public static final long REDACT_NONE = 0L; // 0x0L
-    field public static final int TRANSPORT_TEST = 7; // 0x7
-  }
-
-  public static final class NetworkCapabilities.Builder {
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setAllowedUids(@NonNull java.util.Set<java.lang.Integer>);
-    method @NonNull public android.net.NetworkCapabilities.Builder setUids(@Nullable java.util.Set<android.util.Range<java.lang.Integer>>);
-  }
-
-  public class NetworkRequest implements android.os.Parcelable {
-    method @NonNull public int[] getEnterpriseIds();
-    method @NonNull public int[] getForbiddenCapabilities();
-    method public boolean hasEnterpriseId(int);
-    method public boolean hasForbiddenCapability(int);
-  }
-
-  public static class NetworkRequest.Builder {
-    method @NonNull public android.net.NetworkRequest.Builder addForbiddenCapability(int);
-    method @NonNull public android.net.NetworkRequest.Builder removeForbiddenCapability(int);
-    method @NonNull public android.net.NetworkRequest.Builder setUids(@Nullable java.util.Set<android.util.Range<java.lang.Integer>>);
-  }
-
-  public final class ProfileNetworkPreference implements android.os.Parcelable {
-    method public int describeContents();
-    method @NonNull public int[] getExcludedUids();
-    method @NonNull public int[] getIncludedUids();
-    method public int getPreference();
-    method public int getPreferenceEnterpriseId();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.ProfileNetworkPreference> CREATOR;
-  }
-
-  public static final class ProfileNetworkPreference.Builder {
-    ctor public ProfileNetworkPreference.Builder();
-    method @NonNull public android.net.ProfileNetworkPreference build();
-    method @NonNull public android.net.ProfileNetworkPreference.Builder setExcludedUids(@NonNull int[]);
-    method @NonNull public android.net.ProfileNetworkPreference.Builder setIncludedUids(@NonNull int[]);
-    method @NonNull public android.net.ProfileNetworkPreference.Builder setPreference(int);
-    method @NonNull public android.net.ProfileNetworkPreference.Builder setPreferenceEnterpriseId(int);
-  }
-
-  public final class TestNetworkInterface implements android.os.Parcelable {
-    ctor public TestNetworkInterface(@NonNull android.os.ParcelFileDescriptor, @NonNull String);
-    method public int describeContents();
-    method @NonNull public android.os.ParcelFileDescriptor getFileDescriptor();
-    method @NonNull public String getInterfaceName();
-    method @Nullable public android.net.MacAddress getMacAddress();
-    method public int getMtu();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.TestNetworkInterface> CREATOR;
-  }
-
-  public class TestNetworkManager {
-    method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TEST_NETWORKS) public android.net.TestNetworkInterface createTapInterface();
-    method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TEST_NETWORKS) public android.net.TestNetworkInterface createTunInterface(@NonNull java.util.Collection<android.net.LinkAddress>);
-    method @RequiresPermission(android.Manifest.permission.MANAGE_TEST_NETWORKS) public void setupTestNetwork(@NonNull String, @NonNull android.os.IBinder);
-    method @RequiresPermission(android.Manifest.permission.MANAGE_TEST_NETWORKS) public void teardownTestNetwork(@NonNull android.net.Network);
-    field public static final String TEST_TAP_PREFIX = "testtap";
-  }
-
-  public final class TestNetworkSpecifier extends android.net.NetworkSpecifier implements android.os.Parcelable {
-    ctor public TestNetworkSpecifier(@NonNull String);
-    method public int describeContents();
-    method @Nullable public String getInterfaceName();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.TestNetworkSpecifier> CREATOR;
-  }
-
-  public interface TransportInfo {
-    method public default long getApplicableRedactions();
-    method @NonNull public default android.net.TransportInfo makeCopy(long);
-  }
-
-  public final class VpnTransportInfo implements android.os.Parcelable android.net.TransportInfo {
-    ctor @Deprecated public VpnTransportInfo(int, @Nullable String);
-    method @Nullable public String getSessionId();
-    method @NonNull public android.net.VpnTransportInfo makeCopy(long);
-  }
-
-}
-
diff --git a/framework/cronet_disabled/api/module-lib-removed.txt b/framework/cronet_disabled/api/module-lib-removed.txt
deleted file mode 100644
index d802177..0000000
--- a/framework/cronet_disabled/api/module-lib-removed.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 2.0
diff --git a/framework/cronet_disabled/api/removed.txt b/framework/cronet_disabled/api/removed.txt
deleted file mode 100644
index 303a1e6..0000000
--- a/framework/cronet_disabled/api/removed.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-// Signature format: 2.0
-package android.net {
-
-  public class ConnectivityManager {
-    method @Deprecated public boolean requestRouteToHost(int, int);
-    method @Deprecated public int startUsingNetworkFeature(int, String);
-    method @Deprecated public int stopUsingNetworkFeature(int, String);
-  }
-
-}
-
diff --git a/framework/cronet_disabled/api/system-current.txt b/framework/cronet_disabled/api/system-current.txt
deleted file mode 100644
index 4a2ed8a..0000000
--- a/framework/cronet_disabled/api/system-current.txt
+++ /dev/null
@@ -1,544 +0,0 @@
-// Signature format: 2.0
-package android.net {
-
-  public class CaptivePortal implements android.os.Parcelable {
-    method @Deprecated public void logEvent(int, @NonNull String);
-    method @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public void reevaluateNetwork();
-    method public void useNetwork();
-    field public static final int APP_REQUEST_REEVALUATION_REQUIRED = 100; // 0x64
-    field public static final int APP_RETURN_DISMISSED = 0; // 0x0
-    field public static final int APP_RETURN_UNWANTED = 1; // 0x1
-    field public static final int APP_RETURN_WANTED_AS_IS = 2; // 0x2
-  }
-
-  public final class CaptivePortalData implements android.os.Parcelable {
-    method public int describeContents();
-    method public long getByteLimit();
-    method public long getExpiryTimeMillis();
-    method public long getRefreshTimeMillis();
-    method @Nullable public android.net.Uri getUserPortalUrl();
-    method public int getUserPortalUrlSource();
-    method @Nullable public CharSequence getVenueFriendlyName();
-    method @Nullable public android.net.Uri getVenueInfoUrl();
-    method public int getVenueInfoUrlSource();
-    method public boolean isCaptive();
-    method public boolean isSessionExtendable();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field public static final int CAPTIVE_PORTAL_DATA_SOURCE_OTHER = 0; // 0x0
-    field public static final int CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT = 1; // 0x1
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.CaptivePortalData> CREATOR;
-  }
-
-  public static class CaptivePortalData.Builder {
-    ctor public CaptivePortalData.Builder();
-    ctor public CaptivePortalData.Builder(@Nullable android.net.CaptivePortalData);
-    method @NonNull public android.net.CaptivePortalData build();
-    method @NonNull public android.net.CaptivePortalData.Builder setBytesRemaining(long);
-    method @NonNull public android.net.CaptivePortalData.Builder setCaptive(boolean);
-    method @NonNull public android.net.CaptivePortalData.Builder setExpiryTime(long);
-    method @NonNull public android.net.CaptivePortalData.Builder setRefreshTime(long);
-    method @NonNull public android.net.CaptivePortalData.Builder setSessionExtendable(boolean);
-    method @NonNull public android.net.CaptivePortalData.Builder setUserPortalUrl(@Nullable android.net.Uri);
-    method @NonNull public android.net.CaptivePortalData.Builder setUserPortalUrl(@Nullable android.net.Uri, int);
-    method @NonNull public android.net.CaptivePortalData.Builder setVenueFriendlyName(@Nullable CharSequence);
-    method @NonNull public android.net.CaptivePortalData.Builder setVenueInfoUrl(@Nullable android.net.Uri);
-    method @NonNull public android.net.CaptivePortalData.Builder setVenueInfoUrl(@Nullable android.net.Uri, int);
-  }
-
-  public class ConnectivityManager {
-    method @NonNull @RequiresPermission(android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD) public android.net.SocketKeepalive createNattKeepalive(@NonNull android.net.Network, @NonNull android.os.ParcelFileDescriptor, @NonNull java.net.InetAddress, @NonNull java.net.InetAddress, @NonNull java.util.concurrent.Executor, @NonNull android.net.SocketKeepalive.Callback);
-    method @NonNull @RequiresPermission(android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD) public android.net.SocketKeepalive createSocketKeepalive(@NonNull android.net.Network, @NonNull java.net.Socket, @NonNull java.util.concurrent.Executor, @NonNull android.net.SocketKeepalive.Callback);
-    method @Deprecated @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS) public String getCaptivePortalServerUrl();
-    method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void getLatestTetheringEntitlementResult(int, boolean, @NonNull java.util.concurrent.Executor, @NonNull android.net.ConnectivityManager.OnTetheringEntitlementResultListener);
-    method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public boolean isTetheringSupported();
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_FACTORY}) public int registerNetworkProvider(@NonNull android.net.NetworkProvider);
-    method public void registerQosCallback(@NonNull android.net.QosSocketInfo, @NonNull java.util.concurrent.Executor, @NonNull android.net.QosCallback);
-    method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void registerTetheringEventCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.ConnectivityManager.OnTetheringEventCallback);
-    method @RequiresPermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK) public void requestNetwork(@NonNull android.net.NetworkRequest, int, int, @NonNull android.os.Handler, @NonNull android.net.ConnectivityManager.NetworkCallback);
-    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_AIRPLANE_MODE, android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK}) public void setAirplaneMode(boolean);
-    method @RequiresPermission(android.Manifest.permission.CONTROL_OEM_PAID_NETWORK_PREFERENCE) public void setOemNetworkPreference(@NonNull android.net.OemNetworkPreferences, @Nullable java.util.concurrent.Executor, @Nullable Runnable);
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public boolean shouldAvoidBadWifi();
-    method @RequiresPermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK) public void startCaptivePortalApp(@NonNull android.net.Network, @NonNull android.os.Bundle);
-    method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void startTethering(int, boolean, android.net.ConnectivityManager.OnStartTetheringCallback);
-    method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void startTethering(int, boolean, android.net.ConnectivityManager.OnStartTetheringCallback, android.os.Handler);
-    method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void stopTethering(int);
-    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_FACTORY}) public void unregisterNetworkProvider(@NonNull android.net.NetworkProvider);
-    method public void unregisterQosCallback(@NonNull android.net.QosCallback);
-    method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void unregisterTetheringEventCallback(@NonNull android.net.ConnectivityManager.OnTetheringEventCallback);
-    field public static final String EXTRA_CAPTIVE_PORTAL_PROBE_SPEC = "android.net.extra.CAPTIVE_PORTAL_PROBE_SPEC";
-    field public static final String EXTRA_CAPTIVE_PORTAL_USER_AGENT = "android.net.extra.CAPTIVE_PORTAL_USER_AGENT";
-    field public static final int TETHERING_BLUETOOTH = 2; // 0x2
-    field public static final int TETHERING_USB = 1; // 0x1
-    field public static final int TETHERING_WIFI = 0; // 0x0
-    field @Deprecated public static final int TETHER_ERROR_ENTITLEMENT_UNKONWN = 13; // 0xd
-    field @Deprecated public static final int TETHER_ERROR_NO_ERROR = 0; // 0x0
-    field @Deprecated public static final int TETHER_ERROR_PROVISION_FAILED = 11; // 0xb
-    field public static final int TYPE_NONE = -1; // 0xffffffff
-    field @Deprecated public static final int TYPE_PROXY = 16; // 0x10
-    field @Deprecated public static final int TYPE_WIFI_P2P = 13; // 0xd
-  }
-
-  @Deprecated public abstract static class ConnectivityManager.OnStartTetheringCallback {
-    ctor @Deprecated public ConnectivityManager.OnStartTetheringCallback();
-    method @Deprecated public void onTetheringFailed();
-    method @Deprecated public void onTetheringStarted();
-  }
-
-  @Deprecated public static interface ConnectivityManager.OnTetheringEntitlementResultListener {
-    method @Deprecated public void onTetheringEntitlementResult(int);
-  }
-
-  @Deprecated public abstract static class ConnectivityManager.OnTetheringEventCallback {
-    ctor @Deprecated public ConnectivityManager.OnTetheringEventCallback();
-    method @Deprecated public void onUpstreamChanged(@Nullable android.net.Network);
-  }
-
-  public final class DscpPolicy implements android.os.Parcelable {
-    method @Nullable public java.net.InetAddress getDestinationAddress();
-    method @Nullable public android.util.Range<java.lang.Integer> getDestinationPortRange();
-    method public int getDscpValue();
-    method public int getPolicyId();
-    method public int getProtocol();
-    method @Nullable public java.net.InetAddress getSourceAddress();
-    method public int getSourcePort();
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.DscpPolicy> CREATOR;
-    field public static final int PROTOCOL_ANY = -1; // 0xffffffff
-    field public static final int SOURCE_PORT_ANY = -1; // 0xffffffff
-  }
-
-  public static final class DscpPolicy.Builder {
-    ctor public DscpPolicy.Builder(int, int);
-    method @NonNull public android.net.DscpPolicy build();
-    method @NonNull public android.net.DscpPolicy.Builder setDestinationAddress(@NonNull java.net.InetAddress);
-    method @NonNull public android.net.DscpPolicy.Builder setDestinationPortRange(@NonNull android.util.Range<java.lang.Integer>);
-    method @NonNull public android.net.DscpPolicy.Builder setProtocol(int);
-    method @NonNull public android.net.DscpPolicy.Builder setSourceAddress(@NonNull java.net.InetAddress);
-    method @NonNull public android.net.DscpPolicy.Builder setSourcePort(int);
-  }
-
-  public final class InvalidPacketException extends java.lang.Exception {
-    ctor public InvalidPacketException(int);
-    method public int getError();
-    field public static final int ERROR_INVALID_IP_ADDRESS = -21; // 0xffffffeb
-    field public static final int ERROR_INVALID_LENGTH = -23; // 0xffffffe9
-    field public static final int ERROR_INVALID_PORT = -22; // 0xffffffea
-  }
-
-  public final class IpConfiguration implements android.os.Parcelable {
-    ctor public IpConfiguration();
-    ctor public IpConfiguration(@NonNull android.net.IpConfiguration);
-    method @NonNull public android.net.IpConfiguration.IpAssignment getIpAssignment();
-    method @NonNull public android.net.IpConfiguration.ProxySettings getProxySettings();
-    method public void setHttpProxy(@Nullable android.net.ProxyInfo);
-    method public void setIpAssignment(@NonNull android.net.IpConfiguration.IpAssignment);
-    method public void setProxySettings(@NonNull android.net.IpConfiguration.ProxySettings);
-    method public void setStaticIpConfiguration(@Nullable android.net.StaticIpConfiguration);
-  }
-
-  public enum IpConfiguration.IpAssignment {
-    enum_constant public static final android.net.IpConfiguration.IpAssignment DHCP;
-    enum_constant public static final android.net.IpConfiguration.IpAssignment STATIC;
-    enum_constant public static final android.net.IpConfiguration.IpAssignment UNASSIGNED;
-  }
-
-  public enum IpConfiguration.ProxySettings {
-    enum_constant public static final android.net.IpConfiguration.ProxySettings NONE;
-    enum_constant public static final android.net.IpConfiguration.ProxySettings PAC;
-    enum_constant public static final android.net.IpConfiguration.ProxySettings STATIC;
-    enum_constant public static final android.net.IpConfiguration.ProxySettings UNASSIGNED;
-  }
-
-  public final class IpPrefix implements android.os.Parcelable {
-    ctor public IpPrefix(@NonNull String);
-  }
-
-  public class KeepalivePacketData {
-    ctor protected KeepalivePacketData(@NonNull java.net.InetAddress, @IntRange(from=0, to=65535) int, @NonNull java.net.InetAddress, @IntRange(from=0, to=65535) int, @NonNull byte[]) throws android.net.InvalidPacketException;
-    method @NonNull public java.net.InetAddress getDstAddress();
-    method public int getDstPort();
-    method @NonNull public byte[] getPacket();
-    method @NonNull public java.net.InetAddress getSrcAddress();
-    method public int getSrcPort();
-  }
-
-  public class LinkAddress implements android.os.Parcelable {
-    ctor public LinkAddress(@NonNull java.net.InetAddress, @IntRange(from=0, to=128) int, int, int);
-    ctor public LinkAddress(@NonNull java.net.InetAddress, @IntRange(from=0, to=128) int, int, int, long, long);
-    ctor public LinkAddress(@NonNull java.net.InetAddress, @IntRange(from=0, to=128) int);
-    ctor public LinkAddress(@NonNull String);
-    ctor public LinkAddress(@NonNull String, int, int);
-    method public long getDeprecationTime();
-    method public long getExpirationTime();
-    method public boolean isGlobalPreferred();
-    method public boolean isIpv4();
-    method public boolean isIpv6();
-    method public boolean isSameAddressAs(@Nullable android.net.LinkAddress);
-    field public static final long LIFETIME_PERMANENT = 9223372036854775807L; // 0x7fffffffffffffffL
-    field public static final long LIFETIME_UNKNOWN = -1L; // 0xffffffffffffffffL
-  }
-
-  public final class LinkProperties implements android.os.Parcelable {
-    ctor public LinkProperties(@Nullable android.net.LinkProperties);
-    ctor public LinkProperties(@Nullable android.net.LinkProperties, boolean);
-    method public boolean addDnsServer(@NonNull java.net.InetAddress);
-    method public boolean addLinkAddress(@NonNull android.net.LinkAddress);
-    method public boolean addPcscfServer(@NonNull java.net.InetAddress);
-    method @NonNull public java.util.List<java.net.InetAddress> getAddresses();
-    method @NonNull public java.util.List<java.lang.String> getAllInterfaceNames();
-    method @NonNull public java.util.List<android.net.LinkAddress> getAllLinkAddresses();
-    method @NonNull public java.util.List<android.net.RouteInfo> getAllRoutes();
-    method @Nullable public android.net.Uri getCaptivePortalApiUrl();
-    method @Nullable public android.net.CaptivePortalData getCaptivePortalData();
-    method @NonNull public java.util.List<java.net.InetAddress> getPcscfServers();
-    method @Nullable public String getTcpBufferSizes();
-    method @NonNull public java.util.List<java.net.InetAddress> getValidatedPrivateDnsServers();
-    method public boolean hasGlobalIpv6Address();
-    method public boolean hasIpv4Address();
-    method public boolean hasIpv4DefaultRoute();
-    method public boolean hasIpv4DnsServer();
-    method public boolean hasIpv6DefaultRoute();
-    method public boolean hasIpv6DnsServer();
-    method public boolean isIpv4Provisioned();
-    method public boolean isIpv6Provisioned();
-    method public boolean isProvisioned();
-    method public boolean isReachable(@NonNull java.net.InetAddress);
-    method public boolean removeDnsServer(@NonNull java.net.InetAddress);
-    method public boolean removeLinkAddress(@NonNull android.net.LinkAddress);
-    method public boolean removeRoute(@NonNull android.net.RouteInfo);
-    method public void setCaptivePortalApiUrl(@Nullable android.net.Uri);
-    method public void setCaptivePortalData(@Nullable android.net.CaptivePortalData);
-    method public void setPcscfServers(@NonNull java.util.Collection<java.net.InetAddress>);
-    method public void setPrivateDnsServerName(@Nullable String);
-    method public void setTcpBufferSizes(@Nullable String);
-    method public void setUsePrivateDns(boolean);
-    method public void setValidatedPrivateDnsServers(@NonNull java.util.Collection<java.net.InetAddress>);
-  }
-
-  public final class NattKeepalivePacketData extends android.net.KeepalivePacketData implements android.os.Parcelable {
-    ctor public NattKeepalivePacketData(@NonNull java.net.InetAddress, int, @NonNull java.net.InetAddress, int, @NonNull byte[]) throws android.net.InvalidPacketException;
-    method public int describeContents();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.NattKeepalivePacketData> CREATOR;
-  }
-
-  public class Network implements android.os.Parcelable {
-    ctor public Network(@NonNull android.net.Network);
-    method public int getNetId();
-    method @NonNull public android.net.Network getPrivateDnsBypassingCopy();
-  }
-
-  public abstract class NetworkAgent {
-    ctor public NetworkAgent(@NonNull android.content.Context, @NonNull android.os.Looper, @NonNull String, @NonNull android.net.NetworkCapabilities, @NonNull android.net.LinkProperties, int, @NonNull android.net.NetworkAgentConfig, @Nullable android.net.NetworkProvider);
-    ctor public NetworkAgent(@NonNull android.content.Context, @NonNull android.os.Looper, @NonNull String, @NonNull android.net.NetworkCapabilities, @NonNull android.net.LinkProperties, @NonNull android.net.NetworkScore, @NonNull android.net.NetworkAgentConfig, @Nullable android.net.NetworkProvider);
-    method @Nullable public android.net.Network getNetwork();
-    method public void markConnected();
-    method public void onAddKeepalivePacketFilter(int, @NonNull android.net.KeepalivePacketData);
-    method public void onAutomaticReconnectDisabled();
-    method public void onBandwidthUpdateRequested();
-    method public void onDscpPolicyStatusUpdated(int, int);
-    method public void onNetworkCreated();
-    method public void onNetworkDestroyed();
-    method public void onNetworkUnwanted();
-    method public void onQosCallbackRegistered(int, @NonNull android.net.QosFilter);
-    method public void onQosCallbackUnregistered(int);
-    method public void onRemoveKeepalivePacketFilter(int);
-    method public void onSaveAcceptUnvalidated(boolean);
-    method public void onSignalStrengthThresholdsUpdated(@NonNull int[]);
-    method public void onStartSocketKeepalive(int, @NonNull java.time.Duration, @NonNull android.net.KeepalivePacketData);
-    method public void onStopSocketKeepalive(int);
-    method public void onValidationStatus(int, @Nullable android.net.Uri);
-    method @NonNull public android.net.Network register();
-    method public void sendAddDscpPolicy(@NonNull android.net.DscpPolicy);
-    method public void sendLinkProperties(@NonNull android.net.LinkProperties);
-    method public void sendNetworkCapabilities(@NonNull android.net.NetworkCapabilities);
-    method public void sendNetworkScore(@NonNull android.net.NetworkScore);
-    method public void sendNetworkScore(@IntRange(from=0, to=99) int);
-    method public final void sendQosCallbackError(int, int);
-    method public final void sendQosSessionAvailable(int, int, @NonNull android.net.QosSessionAttributes);
-    method public final void sendQosSessionLost(int, int, int);
-    method public void sendRemoveAllDscpPolicies();
-    method public void sendRemoveDscpPolicy(int);
-    method public final void sendSocketKeepaliveEvent(int, int);
-    method @Deprecated public void setLegacySubtype(int, @NonNull String);
-    method public void setLingerDuration(@NonNull java.time.Duration);
-    method public void setTeardownDelayMillis(@IntRange(from=0, to=0x1388) int);
-    method public void setUnderlyingNetworks(@Nullable java.util.List<android.net.Network>);
-    method public void unregister();
-    method public void unregisterAfterReplacement(@IntRange(from=0, to=0x1388) int);
-    field public static final int DSCP_POLICY_STATUS_DELETED = 4; // 0x4
-    field public static final int DSCP_POLICY_STATUS_INSUFFICIENT_PROCESSING_RESOURCES = 3; // 0x3
-    field public static final int DSCP_POLICY_STATUS_POLICY_NOT_FOUND = 5; // 0x5
-    field public static final int DSCP_POLICY_STATUS_REQUESTED_CLASSIFIER_NOT_SUPPORTED = 2; // 0x2
-    field public static final int DSCP_POLICY_STATUS_REQUEST_DECLINED = 1; // 0x1
-    field public static final int DSCP_POLICY_STATUS_SUCCESS = 0; // 0x0
-    field public static final int VALIDATION_STATUS_NOT_VALID = 2; // 0x2
-    field public static final int VALIDATION_STATUS_VALID = 1; // 0x1
-  }
-
-  public final class NetworkAgentConfig implements android.os.Parcelable {
-    method public int describeContents();
-    method public int getLegacyType();
-    method @NonNull public String getLegacyTypeName();
-    method public boolean isExplicitlySelected();
-    method public boolean isPartialConnectivityAcceptable();
-    method public boolean isUnvalidatedConnectivityAcceptable();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkAgentConfig> CREATOR;
-  }
-
-  public static final class NetworkAgentConfig.Builder {
-    ctor public NetworkAgentConfig.Builder();
-    method @NonNull public android.net.NetworkAgentConfig build();
-    method @NonNull public android.net.NetworkAgentConfig.Builder setExplicitlySelected(boolean);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setLegacyExtraInfo(@NonNull String);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setLegacySubType(int);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setLegacySubTypeName(@NonNull String);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setLegacyType(int);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setLegacyTypeName(@NonNull String);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setNat64DetectionEnabled(boolean);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setPartialConnectivityAcceptable(boolean);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setProvisioningNotificationEnabled(boolean);
-    method @NonNull public android.net.NetworkAgentConfig.Builder setUnvalidatedConnectivityAcceptable(boolean);
-  }
-
-  public final class NetworkCapabilities implements android.os.Parcelable {
-    method @NonNull public int[] getAdministratorUids();
-    method @Nullable public static String getCapabilityCarrierName(int);
-    method @Nullable public String getSsid();
-    method @NonNull public java.util.Set<java.lang.Integer> getSubscriptionIds();
-    method @NonNull public int[] getTransportTypes();
-    method @Nullable public java.util.List<android.net.Network> getUnderlyingNetworks();
-    method public boolean isPrivateDnsBroken();
-    method public boolean satisfiedByNetworkCapabilities(@Nullable android.net.NetworkCapabilities);
-    field public static final int NET_CAPABILITY_BIP = 31; // 0x1f
-    field public static final int NET_CAPABILITY_NOT_VCN_MANAGED = 28; // 0x1c
-    field public static final int NET_CAPABILITY_OEM_PAID = 22; // 0x16
-    field public static final int NET_CAPABILITY_OEM_PRIVATE = 26; // 0x1a
-    field public static final int NET_CAPABILITY_PARTIAL_CONNECTIVITY = 24; // 0x18
-    field public static final int NET_CAPABILITY_VEHICLE_INTERNAL = 27; // 0x1b
-    field public static final int NET_CAPABILITY_VSIM = 30; // 0x1e
-  }
-
-  public static final class NetworkCapabilities.Builder {
-    ctor public NetworkCapabilities.Builder();
-    ctor public NetworkCapabilities.Builder(@NonNull android.net.NetworkCapabilities);
-    method @NonNull public android.net.NetworkCapabilities.Builder addCapability(int);
-    method @NonNull public android.net.NetworkCapabilities.Builder addEnterpriseId(int);
-    method @NonNull public android.net.NetworkCapabilities.Builder addTransportType(int);
-    method @NonNull public android.net.NetworkCapabilities build();
-    method @NonNull public android.net.NetworkCapabilities.Builder removeCapability(int);
-    method @NonNull public android.net.NetworkCapabilities.Builder removeEnterpriseId(int);
-    method @NonNull public android.net.NetworkCapabilities.Builder removeTransportType(int);
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setAdministratorUids(@NonNull int[]);
-    method @NonNull public android.net.NetworkCapabilities.Builder setLinkDownstreamBandwidthKbps(int);
-    method @NonNull public android.net.NetworkCapabilities.Builder setLinkUpstreamBandwidthKbps(int);
-    method @NonNull public android.net.NetworkCapabilities.Builder setNetworkSpecifier(@Nullable android.net.NetworkSpecifier);
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setOwnerUid(int);
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setRequestorPackageName(@Nullable String);
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setRequestorUid(int);
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP) public android.net.NetworkCapabilities.Builder setSignalStrength(int);
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setSsid(@Nullable String);
-    method @NonNull public android.net.NetworkCapabilities.Builder setSubscriptionIds(@NonNull java.util.Set<java.lang.Integer>);
-    method @NonNull public android.net.NetworkCapabilities.Builder setTransportInfo(@Nullable android.net.TransportInfo);
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setUnderlyingNetworks(@Nullable java.util.List<android.net.Network>);
-    method @NonNull public static android.net.NetworkCapabilities.Builder withoutDefaultCapabilities();
-  }
-
-  public class NetworkProvider {
-    ctor public NetworkProvider(@NonNull android.content.Context, @NonNull android.os.Looper, @NonNull String);
-    method @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public void declareNetworkRequestUnfulfillable(@NonNull android.net.NetworkRequest);
-    method public int getProviderId();
-    method public void onNetworkRequestWithdrawn(@NonNull android.net.NetworkRequest);
-    method public void onNetworkRequested(@NonNull android.net.NetworkRequest, @IntRange(from=0, to=99) int, int);
-    method @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public void registerNetworkOffer(@NonNull android.net.NetworkScore, @NonNull android.net.NetworkCapabilities, @NonNull java.util.concurrent.Executor, @NonNull android.net.NetworkProvider.NetworkOfferCallback);
-    method @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public void unregisterNetworkOffer(@NonNull android.net.NetworkProvider.NetworkOfferCallback);
-    field public static final int ID_NONE = -1; // 0xffffffff
-  }
-
-  public static interface NetworkProvider.NetworkOfferCallback {
-    method public void onNetworkNeeded(@NonNull android.net.NetworkRequest);
-    method public void onNetworkUnneeded(@NonNull android.net.NetworkRequest);
-  }
-
-  public class NetworkReleasedException extends java.lang.Exception {
-    ctor public NetworkReleasedException();
-  }
-
-  public class NetworkRequest implements android.os.Parcelable {
-    method @Nullable public String getRequestorPackageName();
-    method public int getRequestorUid();
-  }
-
-  public static class NetworkRequest.Builder {
-    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP) public android.net.NetworkRequest.Builder setSignalStrength(int);
-    method @NonNull public android.net.NetworkRequest.Builder setSubscriptionIds(@NonNull java.util.Set<java.lang.Integer>);
-  }
-
-  public final class NetworkScore implements android.os.Parcelable {
-    method public int describeContents();
-    method public int getKeepConnectedReason();
-    method public int getLegacyInt();
-    method public boolean isExiting();
-    method public boolean isTransportPrimary();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkScore> CREATOR;
-    field public static final int KEEP_CONNECTED_FOR_HANDOVER = 1; // 0x1
-    field public static final int KEEP_CONNECTED_NONE = 0; // 0x0
-  }
-
-  public static final class NetworkScore.Builder {
-    ctor public NetworkScore.Builder();
-    method @NonNull public android.net.NetworkScore build();
-    method @NonNull public android.net.NetworkScore.Builder setExiting(boolean);
-    method @NonNull public android.net.NetworkScore.Builder setKeepConnectedReason(int);
-    method @NonNull public android.net.NetworkScore.Builder setLegacyInt(int);
-    method @NonNull public android.net.NetworkScore.Builder setTransportPrimary(boolean);
-  }
-
-  public final class OemNetworkPreferences implements android.os.Parcelable {
-    method public int describeContents();
-    method @NonNull public java.util.Map<java.lang.String,java.lang.Integer> getNetworkPreferences();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.OemNetworkPreferences> CREATOR;
-    field public static final int OEM_NETWORK_PREFERENCE_OEM_PAID = 1; // 0x1
-    field public static final int OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK = 2; // 0x2
-    field public static final int OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY = 3; // 0x3
-    field public static final int OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY = 4; // 0x4
-    field public static final int OEM_NETWORK_PREFERENCE_UNINITIALIZED = 0; // 0x0
-  }
-
-  public static final class OemNetworkPreferences.Builder {
-    ctor public OemNetworkPreferences.Builder();
-    ctor public OemNetworkPreferences.Builder(@NonNull android.net.OemNetworkPreferences);
-    method @NonNull public android.net.OemNetworkPreferences.Builder addNetworkPreference(@NonNull String, int);
-    method @NonNull public android.net.OemNetworkPreferences build();
-    method @NonNull public android.net.OemNetworkPreferences.Builder clearNetworkPreference(@NonNull String);
-  }
-
-  public abstract class QosCallback {
-    ctor public QosCallback();
-    method public void onError(@NonNull android.net.QosCallbackException);
-    method public void onQosSessionAvailable(@NonNull android.net.QosSession, @NonNull android.net.QosSessionAttributes);
-    method public void onQosSessionLost(@NonNull android.net.QosSession);
-  }
-
-  public static class QosCallback.QosCallbackRegistrationException extends java.lang.RuntimeException {
-  }
-
-  public final class QosCallbackException extends java.lang.Exception {
-    ctor public QosCallbackException(@NonNull String);
-    ctor public QosCallbackException(@NonNull Throwable);
-  }
-
-  public abstract class QosFilter {
-    method @NonNull public abstract android.net.Network getNetwork();
-    method public abstract boolean matchesLocalAddress(@NonNull java.net.InetAddress, int, int);
-    method public boolean matchesProtocol(int);
-    method public abstract boolean matchesRemoteAddress(@NonNull java.net.InetAddress, int, int);
-  }
-
-  public final class QosSession implements android.os.Parcelable {
-    ctor public QosSession(int, int);
-    method public int describeContents();
-    method public int getSessionId();
-    method public int getSessionType();
-    method public long getUniqueId();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.QosSession> CREATOR;
-    field public static final int TYPE_EPS_BEARER = 1; // 0x1
-    field public static final int TYPE_NR_BEARER = 2; // 0x2
-  }
-
-  public interface QosSessionAttributes {
-  }
-
-  public final class QosSocketInfo implements android.os.Parcelable {
-    ctor public QosSocketInfo(@NonNull android.net.Network, @NonNull java.net.Socket) throws java.io.IOException;
-    ctor public QosSocketInfo(@NonNull android.net.Network, @NonNull java.net.DatagramSocket) throws java.io.IOException;
-    method public int describeContents();
-    method @NonNull public java.net.InetSocketAddress getLocalSocketAddress();
-    method @NonNull public android.net.Network getNetwork();
-    method @Nullable public java.net.InetSocketAddress getRemoteSocketAddress();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.QosSocketInfo> CREATOR;
-  }
-
-  public final class RouteInfo implements android.os.Parcelable {
-    ctor public RouteInfo(@Nullable android.net.IpPrefix, @Nullable java.net.InetAddress, @Nullable String, int);
-    ctor public RouteInfo(@Nullable android.net.IpPrefix, @Nullable java.net.InetAddress, @Nullable String, int, int);
-    method public int getMtu();
-  }
-
-  public abstract class SocketKeepalive implements java.lang.AutoCloseable {
-    method public final void start(@IntRange(from=0xa, to=0xe10) int, int, @Nullable android.net.Network);
-    field public static final int ERROR_NO_SUCH_SLOT = -33; // 0xffffffdf
-    field public static final int FLAG_AUTOMATIC_ON_OFF = 1; // 0x1
-    field public static final int SUCCESS = 0; // 0x0
-  }
-
-  public class SocketLocalAddressChangedException extends java.lang.Exception {
-    ctor public SocketLocalAddressChangedException();
-  }
-
-  public class SocketNotBoundException extends java.lang.Exception {
-    ctor public SocketNotBoundException();
-  }
-
-  public class SocketNotConnectedException extends java.lang.Exception {
-    ctor public SocketNotConnectedException();
-  }
-
-  public class SocketRemoteAddressChangedException extends java.lang.Exception {
-    ctor public SocketRemoteAddressChangedException();
-  }
-
-  public final class StaticIpConfiguration implements android.os.Parcelable {
-    ctor public StaticIpConfiguration();
-    ctor public StaticIpConfiguration(@Nullable android.net.StaticIpConfiguration);
-    method public void addDnsServer(@NonNull java.net.InetAddress);
-    method public void clear();
-    method @NonNull public java.util.List<android.net.RouteInfo> getRoutes(@Nullable String);
-  }
-
-  public final class TcpKeepalivePacketData extends android.net.KeepalivePacketData implements android.os.Parcelable {
-    ctor public TcpKeepalivePacketData(@NonNull java.net.InetAddress, int, @NonNull java.net.InetAddress, int, @NonNull byte[], int, int, int, int, int, int) throws android.net.InvalidPacketException;
-    method public int describeContents();
-    method public int getIpTos();
-    method public int getIpTtl();
-    method public int getTcpAck();
-    method public int getTcpSeq();
-    method public int getTcpWindow();
-    method public int getTcpWindowScale();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.TcpKeepalivePacketData> CREATOR;
-  }
-
-  public final class VpnTransportInfo implements android.os.Parcelable android.net.TransportInfo {
-    ctor public VpnTransportInfo(int, @Nullable String, boolean, boolean);
-    method public boolean areLongLivedTcpConnectionsExpensive();
-    method public int describeContents();
-    method public int getType();
-    method public boolean isBypassable();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.VpnTransportInfo> CREATOR;
-  }
-
-}
-
-package android.net.apf {
-
-  public final class ApfCapabilities implements android.os.Parcelable {
-    ctor public ApfCapabilities(int, int, int);
-    method public int describeContents();
-    method public static boolean getApfDrop8023Frames();
-    method @NonNull public static int[] getApfEtherTypeBlackList();
-    method public boolean hasDataAccess();
-    method public void writeToParcel(android.os.Parcel, int);
-    field public static final android.os.Parcelable.Creator<android.net.apf.ApfCapabilities> CREATOR;
-    field public final int apfPacketFormat;
-    field public final int apfVersionSupported;
-    field public final int maximumApfProgramSize;
-  }
-
-}
-
diff --git a/framework/cronet_disabled/api/system-lint-baseline.txt b/framework/cronet_disabled/api/system-lint-baseline.txt
deleted file mode 100644
index 9a97707..0000000
--- a/framework/cronet_disabled/api/system-lint-baseline.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Baseline format: 1.0
diff --git a/framework/cronet_disabled/api/system-removed.txt b/framework/cronet_disabled/api/system-removed.txt
deleted file mode 100644
index d802177..0000000
--- a/framework/cronet_disabled/api/system-removed.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 2.0
diff --git a/framework/src/android/net/ConnectivitySettingsManager.java b/framework/src/android/net/ConnectivitySettingsManager.java
index 67dacb8..ba7df7f 100644
--- a/framework/src/android/net/ConnectivitySettingsManager.java
+++ b/framework/src/android/net/ConnectivitySettingsManager.java
@@ -176,7 +176,9 @@
 
     /**
      * When detecting a captive portal, immediately disconnect from the
-     * network and do not reconnect to that network in the future.
+     * network and do not reconnect to that network in the future; except
+     * on Wear platform companion proxy networks (transport BLUETOOTH)
+     * will stay behind captive portal.
      */
     public static final int CAPTIVE_PORTAL_MODE_AVOID = 2;
 
diff --git a/framework/src/android/net/DnsResolver.java b/framework/src/android/net/DnsResolver.java
index c6034f1..5fefcd6 100644
--- a/framework/src/android/net/DnsResolver.java
+++ b/framework/src/android/net/DnsResolver.java
@@ -77,6 +77,15 @@
     @interface QueryType {}
     public static final int TYPE_A = 1;
     public static final int TYPE_AAAA = 28;
+    // TODO: add below constants as part of QueryType and the public API
+    /** @hide */
+    public static final int TYPE_PTR = 12;
+    /** @hide */
+    public static final int TYPE_TXT = 16;
+    /** @hide */
+    public static final int TYPE_SRV = 33;
+    /** @hide */
+    public static final int TYPE_ANY = 255;
 
     @IntDef(prefix = { "FLAG_" }, value = {
             FLAG_EMPTY,
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index abda1fa..74d50f8 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -29,6 +29,9 @@
 import android.annotation.SystemApi;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.net.ConnectivityManager.NetworkCallback;
+// Can't be imported because aconfig tooling doesn't exist on udc-mainline-prod yet
+// See inner class Flags which mimics this for the time being
+// import android.net.flags.Flags;
 import android.os.Build;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -121,6 +124,14 @@
 public final class NetworkCapabilities implements Parcelable {
     private static final String TAG = "NetworkCapabilities";
 
+    // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
+    // available here
+    /** @hide */
+    public static class Flags {
+        static final String FLAG_FORBIDDEN_CAPABILITY =
+                "com.android.net.flags.forbidden_capability";
+    }
+
     /**
      * Mechanism to support redaction of fields in NetworkCapabilities that are guarded by specific
      * app permissions.
@@ -793,6 +804,10 @@
      * Adds the given capability to this {@code NetworkCapability} instance.
      * Note that when searching for a network to satisfy a request, all capabilities
      * requested must be satisfied.
+     * <p>
+     * If the capability was previously added to the list of forbidden capabilities (either
+     * by default or added using {@link #addForbiddenCapability(int)}), then it will be removed
+     * from the list of forbidden capabilities as well.
      *
      * @param capability the capability to be added.
      * @return This NetworkCapabilities instance, to facilitate chaining.
@@ -801,8 +816,7 @@
     public @NonNull NetworkCapabilities addCapability(@NetCapability int capability) {
         // If the given capability was previously added to the list of forbidden capabilities
         // then the capability will also be removed from the list of forbidden capabilities.
-        // TODO: Consider adding forbidden capabilities to the public API and mention this
-        // in the documentation.
+        // TODO: Add forbidden capabilities to the public API
         checkValidCapability(capability);
         mNetworkCapabilities |= 1L << capability;
         // remove from forbidden capability list
@@ -901,6 +915,8 @@
      * @return an array of forbidden capability values for this instance.
      * @hide
      */
+    @NonNull
+    // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public
     public @NetCapability int[] getForbiddenCapabilities() {
         return BitUtils.unpackBits(mForbiddenNetworkCapabilities);
     }
@@ -1000,7 +1016,7 @@
     /**
      * Tests for the presence of a capability on this instance.
      *
-     * @param capability the capabilities to be tested for.
+     * @param capability the capability to be tested for.
      * @return {@code true} if set on this instance.
      */
     public boolean hasCapability(@NetCapability int capability) {
@@ -1008,8 +1024,15 @@
                 && ((mNetworkCapabilities & (1L << capability)) != 0);
     }
 
-    /** @hide */
+    /**
+     * Tests for the presence of a forbidden capability on this instance.
+     *
+     * @param capability the capability to be tested for.
+     * @return {@code true} if this capability is set forbidden on this instance.
+     * @hide
+     */
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public
     public boolean hasForbiddenCapability(@NetCapability int capability) {
         return isValidCapability(capability)
                 && ((mForbiddenNetworkCapabilities & (1L << capability)) != 0);
@@ -2889,6 +2912,44 @@
         }
 
         /**
+         * Adds the given capability to the list of forbidden capabilities.
+         *
+         * A network with a capability will not match a {@link NetworkCapabilities} or
+         * {@link NetworkRequest} which has said capability set as forbidden. For example, if
+         * a request has NET_CAPABILITY_INTERNET in the list of forbidden capabilities, networks
+         * with NET_CAPABILITY_INTERNET will not match the request.
+         *
+         * If the capability was previously added to the list of required capabilities (for
+         * example, it was there by default or added using {@link #addCapability(int)} method), then
+         * it will be removed from the list of required capabilities as well.
+         *
+         * @param capability the capability
+         * @return this builder
+         * @hide
+         */
+        @NonNull
+        // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public
+        public Builder addForbiddenCapability(@NetCapability final int capability) {
+            mCaps.addForbiddenCapability(capability);
+            return this;
+        }
+
+        /**
+         * Removes the given capability from the list of forbidden capabilities.
+         *
+         * @see #addForbiddenCapability(int)
+         * @param capability the capability
+         * @return this builder
+         * @hide
+         */
+        @NonNull
+        // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public
+        public Builder removeForbiddenCapability(@NetCapability final int capability) {
+            mCaps.removeForbiddenCapability(capability);
+            return this;
+        }
+
+        /**
          * Adds the given enterprise capability identifier.
          * Note that when searching for a network to satisfy a request, all capabilities identifier
          * requested must be satisfied. Enterprise capability identifier is applicable only
@@ -3235,4 +3296,4 @@
             return new NetworkCapabilities(mCaps);
         }
     }
-}
\ No newline at end of file
+}
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index 6c351d0..5cc9f1b 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -39,6 +39,8 @@
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.compat.annotation.UnsupportedAppUsage;
+// TODO : replace with android.net.flags.Flags when aconfig is supported on udc-mainline-prod
+// import android.net.NetworkCapabilities.Flags;
 import android.net.NetworkCapabilities.NetCapability;
 import android.net.NetworkCapabilities.Transport;
 import android.os.Build;
@@ -408,6 +410,7 @@
         @NonNull
         @SuppressLint("MissingGetterMatchingBuilder")
         @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+        // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public
         public Builder addForbiddenCapability(@NetworkCapabilities.NetCapability int capability) {
             mNetworkCapabilities.addForbiddenCapability(capability);
             return this;
@@ -424,6 +427,7 @@
         @NonNull
         @SuppressLint("BuilderSetStyle")
         @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+        // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public
         public Builder removeForbiddenCapability(
                 @NetworkCapabilities.NetCapability int capability) {
             mNetworkCapabilities.removeForbiddenCapability(capability);
@@ -433,6 +437,7 @@
         /**
          * Completely clears all the {@code NetworkCapabilities} from this builder instance,
          * removing even the capabilities that are set by default when the object is constructed.
+         * Also removes any set forbidden capabilities.
          *
          * @return The builder to facilitate chaining.
          */
@@ -721,6 +726,7 @@
      * @hide
      */
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public instead of @SystemApi
     public boolean hasForbiddenCapability(@NetCapability int capability) {
         return networkCapabilities.hasForbiddenCapability(capability);
     }
@@ -843,6 +849,7 @@
      */
     @NonNull
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public instead of @SystemApi
     public @NetCapability int[] getForbiddenCapabilities() {
         // No need to make a defensive copy here as NC#getForbiddenCapabilities() already returns
         // a new array.
diff --git a/framework/src/android/net/NetworkScore.java b/framework/src/android/net/NetworkScore.java
index 815e2b0..2522958 100644
--- a/framework/src/android/net/NetworkScore.java
+++ b/framework/src/android/net/NetworkScore.java
@@ -44,7 +44,8 @@
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(value = {
             KEEP_CONNECTED_NONE,
-            KEEP_CONNECTED_FOR_HANDOVER
+            KEEP_CONNECTED_FOR_HANDOVER,
+            KEEP_CONNECTED_FOR_TEST
     })
     public @interface KeepConnectedReason { }
 
@@ -57,6 +58,12 @@
      * is being considered for handover.
      */
     public static final int KEEP_CONNECTED_FOR_HANDOVER = 1;
+    /**
+     * Keep this network connected even if there is no outstanding request for it, because it
+     * is used in a test and it's not necessarily easy to file the right request for it.
+     * @hide
+     */
+    public static final int KEEP_CONNECTED_FOR_TEST = 2;
 
     // Agent-managed policies
     // This network should lose to a wifi that has ever been validated
diff --git a/nearby/framework/java/android/nearby/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
index a70b303..8b97dc7 100644
--- a/nearby/framework/java/android/nearby/NearbyManager.java
+++ b/nearby/framework/java/android/nearby/NearbyManager.java
@@ -281,6 +281,8 @@
      */
     public void queryOffloadCapability(@NonNull @CallbackExecutor Executor executor,
             @NonNull Consumer<OffloadCapability> callback) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
         try {
             mService.queryOffloadCapability(new OffloadTransport(executor, callback));
         } catch (RemoteException e) {
diff --git a/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
index 9dd07a4..e4651b7 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
@@ -68,15 +68,6 @@
     @GuardedBy("mLock")
     @Nullable
     private List<android.nearby.ScanFilter> mScanFilters;
-    private android.bluetooth.le.ScanCallback mScanCallbackLegacy =
-            new android.bluetooth.le.ScanCallback() {
-                @Override
-                public void onScanResult(int callbackType, ScanResult scanResult) {
-                }
-                @Override
-                public void onScanFailed(int errorCode) {
-                }
-            };
     private android.bluetooth.le.ScanCallback mScanCallback =
             new android.bluetooth.le.ScanCallback() {
                 @Override
@@ -170,7 +161,6 @@
         if (isBleAvailable()) {
             Log.d(TAG, "BleDiscoveryProvider started");
             startScan(getScanFilters(), getScanSettings(/* legacy= */ false), mScanCallback);
-            startScan(getScanFilters(), getScanSettings(/* legacy= */ true), mScanCallbackLegacy);
             return;
         }
         Log.w(TAG, "Cannot start BleDiscoveryProvider because Ble is not available.");
@@ -187,7 +177,6 @@
         }
         Log.v(TAG, "Ble scan stopped.");
         bluetoothLeScanner.stopScan(mScanCallback);
-        bluetoothLeScanner.stopScan(mScanCallbackLegacy);
         synchronized (mLock) {
             if (mScanFilters != null) {
                 mScanFilters = null;
diff --git a/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
index d69d42d..0302e4f 100644
--- a/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
@@ -155,7 +155,7 @@
             builder.setFastPairSupported(version != ChreCommunication.INVALID_NANO_APP_VERSION);
             try {
                 callback.onQueryComplete(builder.build());
-            } catch (RemoteException e) {
+            } catch (RemoteException | NullPointerException e) {
                 e.printStackTrace();
             }
         });
diff --git a/nearby/tests/cts/fastpair/Android.bp b/nearby/tests/cts/fastpair/Android.bp
index a61d180..66a1ffe 100644
--- a/nearby/tests/cts/fastpair/Android.bp
+++ b/nearby/tests/cts/fastpair/Android.bp
@@ -26,7 +26,7 @@
         "bluetooth-test-util-lib",
         "compatibility-device-util-axt",
         "ctstestrunner-axt",
-        "truth-prebuilt",
+        "truth",
     ],
     libs: [
         "android.test.base",
diff --git a/nearby/tests/integration/privileged/Android.bp b/nearby/tests/integration/privileged/Android.bp
index e3250f6..9b6e488 100644
--- a/nearby/tests/integration/privileged/Android.bp
+++ b/nearby/tests/integration/privileged/Android.bp
@@ -27,7 +27,7 @@
         "androidx.test.ext.junit",
         "androidx.test.rules",
         "junit",
-        "truth-prebuilt",
+        "truth",
     ],
     test_suites: ["device-tests"],
 }
diff --git a/nearby/tests/integration/untrusted/Android.bp b/nearby/tests/integration/untrusted/Android.bp
index 57499e4..75f765b 100644
--- a/nearby/tests/integration/untrusted/Android.bp
+++ b/nearby/tests/integration/untrusted/Android.bp
@@ -31,7 +31,7 @@
         "androidx.test.uiautomator_uiautomator",
         "junit",
         "kotlin-test",
-        "truth-prebuilt",
+        "truth",
     ],
     test_suites: ["device-tests"],
 }
diff --git a/nearby/tests/unit/Android.bp b/nearby/tests/unit/Android.bp
index 9b35452..112c751 100644
--- a/nearby/tests/unit/Android.bp
+++ b/nearby/tests/unit/Android.bp
@@ -42,7 +42,7 @@
         "mockito-target-extended-minus-junit4",
         "platform-test-annotations",
         "service-nearby-pre-jarjar",
-        "truth-prebuilt",
+        "truth",
         // "Robolectric_all-target",
     ],
     // these are needed for Extended Mockito
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
index 05b556b..c064844 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.nearby.provider;
 
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.times;
@@ -69,7 +70,7 @@
 
         AdvertiseSettings settings = new AdvertiseSettings.Builder().build();
         mBleBroadcastProvider.onStartSuccess(settings);
-        verify(mBroadcastListener).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
+        verify(mBroadcastListener, times(any())).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
     }
 
     @Test
@@ -81,7 +82,7 @@
         // advertising set can not be mocked, so we will allow nulls
         mBleBroadcastProvider.mAdvertisingSetCallback.onAdvertisingSetStarted(null, -30,
                 AdvertisingSetCallback.ADVERTISE_SUCCESS);
-        verify(mBroadcastListener).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
+        verify(mBroadcastListener, times(any())).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
     }
 
     @Test
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
index 154441b..6f035a9 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
@@ -288,8 +288,12 @@
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
     public void testOnNearbyDeviceDiscoveredWithTestDataElements() {
+        // The feature only supports user-debug builds.
+        if (!Build.isDebuggable()) {
+            return;
+        }
         // Enables the setting of test app support
         boolean isSupportedTestApp = getDeviceConfigBoolean(
                 NEARBY_SUPPORT_TEST_APP, false /* defaultValue */);
diff --git a/netbpfload/Android.bp b/netbpfload/Android.bp
new file mode 100644
index 0000000..5480ef7
--- /dev/null
+++ b/netbpfload/Android.bp
@@ -0,0 +1,48 @@
+//
+// 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.
+//
+
+cc_binary {
+    name: "netbpfload",
+
+    defaults: ["bpf_defaults"],
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wthread-safety",
+    ],
+    sanitize: {
+        integer_overflow: true,
+    },
+
+    header_libs: ["bpf_headers"],
+    shared_libs: [
+        "libbase",
+        "liblog",
+    ],
+    srcs: [
+        "loader.cpp",
+        "NetBpfLoad.cpp",
+    ],
+    apex_available: [
+        "com.android.tethering",
+        "//apex_available:platform",
+    ],
+    // really should be Android 14/U (34), but we cannot include binaries built
+    // against newer sdk in the apex, which still targets 30(R):
+    // module "netbpfload" variant "android_x86_apex30": should support
+    // min_sdk_version(30) for "com.android.tethering": newer SDK(34).
+    min_sdk_version: "30",
+}
diff --git a/netbpfload/NetBpfLoad.cpp b/netbpfload/NetBpfLoad.cpp
new file mode 100644
index 0000000..b44a0bc
--- /dev/null
+++ b/netbpfload/NetBpfLoad.cpp
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2017-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.
+ */
+
+#ifndef LOG_TAG
+#define LOG_TAG "NetBpfLoad"
+#endif
+
+#include <arpa/inet.h>
+#include <dirent.h>
+#include <elf.h>
+#include <error.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <linux/bpf.h>
+#include <linux/unistd.h>
+#include <net/if.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <sys/mman.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <android-base/logging.h>
+#include <android-base/macros.h>
+#include <android-base/properties.h>
+#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
+#include <android-base/unique_fd.h>
+#include <log/log.h>
+
+#include "BpfSyscallWrappers.h"
+#include "bpf/BpfUtils.h"
+#include "loader.h"
+
+using android::base::EndsWith;
+using android::bpf::domain;
+using std::string;
+
+bool exists(const char* const path) {
+    int v = access(path, F_OK);
+    if (!v) {
+        ALOGI("%s exists.", path);
+        return true;
+    }
+    if (errno == ENOENT) return false;
+    ALOGE("FATAL: access(%s, F_OK) -> %d [%d:%s]", path, v, errno, strerror(errno));
+    abort();  // can only hit this if permissions (likely selinux) are screwed up
+}
+
+
+const android::bpf::Location locations[] = {
+        // S+ Tethering mainline module (network_stack): tether offload
+        {
+                .dir = "/apex/com.android.tethering/etc/bpf/",
+                .prefix = "tethering/",
+        },
+        // T+ Tethering mainline module (shared with netd & system server)
+        // netutils_wrapper (for iptables xt_bpf) has access to programs
+        {
+                .dir = "/apex/com.android.tethering/etc/bpf/netd_shared/",
+                .prefix = "netd_shared/",
+        },
+        // T+ Tethering mainline module (shared with netd & system server)
+        // netutils_wrapper has no access, netd has read only access
+        {
+                .dir = "/apex/com.android.tethering/etc/bpf/netd_readonly/",
+                .prefix = "netd_readonly/",
+        },
+        // T+ Tethering mainline module (shared with system server)
+        {
+                .dir = "/apex/com.android.tethering/etc/bpf/net_shared/",
+                .prefix = "net_shared/",
+        },
+        // T+ Tethering mainline module (not shared, just network_stack)
+        {
+                .dir = "/apex/com.android.tethering/etc/bpf/net_private/",
+                .prefix = "net_private/",
+        },
+};
+
+int loadAllElfObjects(const android::bpf::Location& location) {
+    int retVal = 0;
+    DIR* dir;
+    struct dirent* ent;
+
+    if ((dir = opendir(location.dir)) != NULL) {
+        while ((ent = readdir(dir)) != NULL) {
+            string s = ent->d_name;
+            if (!EndsWith(s, ".o")) continue;
+
+            string progPath(location.dir);
+            progPath += s;
+
+            bool critical;
+            int ret = android::bpf::loadProg(progPath.c_str(), &critical, location);
+            if (ret) {
+                if (critical) retVal = ret;
+                ALOGE("Failed to load object: %s, ret: %s", progPath.c_str(), std::strerror(-ret));
+            } else {
+                ALOGI("Loaded object: %s", progPath.c_str());
+            }
+        }
+        closedir(dir);
+    }
+    return retVal;
+}
+
+int createSysFsBpfSubDir(const char* const prefix) {
+    if (*prefix) {
+        mode_t prevUmask = umask(0);
+
+        string s = "/sys/fs/bpf/";
+        s += prefix;
+
+        errno = 0;
+        int ret = mkdir(s.c_str(), S_ISVTX | S_IRWXU | S_IRWXG | S_IRWXO);
+        if (ret && errno != EEXIST) {
+            const int err = errno;
+            ALOGE("Failed to create directory: %s, ret: %s", s.c_str(), std::strerror(err));
+            return -err;
+        }
+
+        umask(prevUmask);
+    }
+    return 0;
+}
+
+// Technically 'value' doesn't need to be newline terminated, but it's best
+// to include a newline to match 'echo "value" > /proc/sys/...foo' behaviour,
+// which is usually how kernel devs test the actual sysctl interfaces.
+int writeProcSysFile(const char *filename, const char *value) {
+    android::base::unique_fd fd(open(filename, O_WRONLY | O_CLOEXEC));
+    if (fd < 0) {
+        const int err = errno;
+        ALOGE("open('%s', O_WRONLY | O_CLOEXEC) -> %s", filename, strerror(err));
+        return -err;
+    }
+    int len = strlen(value);
+    int v = write(fd, value, len);
+    if (v < 0) {
+        const int err = errno;
+        ALOGE("write('%s', '%s', %d) -> %s", filename, value, len, strerror(err));
+        return -err;
+    }
+    if (v != len) {
+        // In practice, due to us only using this for /proc/sys/... files, this can't happen.
+        ALOGE("write('%s', '%s', %d) -> short write [%d]", filename, value, len, v);
+        return -EINVAL;
+    }
+    return 0;
+}
+
+int main(int argc, char** argv) {
+    (void)argc;
+    android::base::InitLogging(argv, &android::base::KernelLogger);
+
+    if (!android::bpf::isAtLeastKernelVersion(4, 19, 0)) {
+        ALOGE("Android U QPR2 requires kernel 4.19.");
+        return 1;
+    }
+
+    if (android::bpf::isUserspace32bit() && android::bpf::isAtLeastKernelVersion(6, 2, 0)) {
+        /* Android 14/U should only launch on 64-bit kernels
+         *   T launches on 5.10/5.15
+         *   U launches on 5.15/6.1
+         * So >=5.16 implies isKernel64Bit()
+         *
+         * We thus added a test to V VTS which requires 5.16+ devices to use 64-bit kernels.
+         *
+         * Starting with Android V, which is the first to support a post 6.1 Linux Kernel,
+         * we also require 64-bit userspace.
+         *
+         * There are various known issues with 32-bit userspace talking to various
+         * kernel interfaces (especially CAP_NET_ADMIN ones) on a 64-bit kernel.
+         * Some of these have userspace or kernel workarounds/hacks.
+         * Some of them don't...
+         * We're going to be removing the hacks.
+         *
+         * Additionally the 32-bit kernel jit support is poor,
+         * and 32-bit userspace on 64-bit kernel bpf ringbuffer compatibility is broken.
+         */
+        ALOGE("64-bit userspace required on 6.2+ kernels.");
+        return 1;
+    }
+
+    // Ensure we can determine the Android build type.
+    if (!android::bpf::isEng() && !android::bpf::isUser() && !android::bpf::isUserdebug()) {
+        ALOGE("Failed to determine the build type: got %s, want 'eng', 'user', or 'userdebug'",
+              android::bpf::getBuildType().c_str());
+        return 1;
+    }
+
+    // Linux 5.16-rc1 changed the default to 2 (disabled but changeable), but we need 0 (enabled)
+    // (this writeFile is known to fail on at least 4.19, but always defaults to 0 on pre-5.13,
+    // on 5.13+ it depends on CONFIG_BPF_UNPRIV_DEFAULT_OFF)
+    if (writeProcSysFile("/proc/sys/kernel/unprivileged_bpf_disabled", "0\n") &&
+        android::bpf::isAtLeastKernelVersion(5, 13, 0)) return 1;
+
+    // Enable the eBPF JIT -- but do note that on 64-bit kernels it is likely
+    // already force enabled by the kernel config option BPF_JIT_ALWAYS_ON.
+    // (Note: this (open) will fail with ENOENT 'No such file or directory' if
+    //  kernel does not have CONFIG_BPF_JIT=y)
+    // BPF_JIT is required by R VINTF (which means 4.14/4.19/5.4 kernels),
+    // but 4.14/4.19 were released with P & Q, and only 5.4 is new in R+.
+    if (writeProcSysFile("/proc/sys/net/core/bpf_jit_enable", "1\n")) return 1;
+
+    // Enable JIT kallsyms export for privileged users only
+    // (Note: this (open) will fail with ENOENT 'No such file or directory' if
+    //  kernel does not have CONFIG_HAVE_EBPF_JIT=y)
+    if (writeProcSysFile("/proc/sys/net/core/bpf_jit_kallsyms", "1\n")) return 1;
+
+    // Create all the pin subdirectories
+    // (this must be done first to allow selinux_context and pin_subdir functionality,
+    //  which could otherwise fail with ENOENT during object pinning or renaming,
+    //  due to ordering issues)
+    for (const auto& location : locations) {
+        if (createSysFsBpfSubDir(location.prefix)) return 1;
+    }
+
+    // Load all ELF objects, create programs and maps, and pin them
+    for (const auto& location : locations) {
+        if (loadAllElfObjects(location) != 0) {
+            ALOGE("=== CRITICAL FAILURE LOADING BPF PROGRAMS FROM %s ===", location.dir);
+            ALOGE("If this triggers reliably, you're probably missing kernel options or patches.");
+            ALOGE("If this triggers randomly, you might be hitting some memory allocation "
+                  "problems or startup script race.");
+            ALOGE("--- DO NOT EXPECT SYSTEM TO BOOT SUCCESSFULLY ---");
+            sleep(20);
+            return 2;
+        }
+    }
+
+    int key = 1;
+    int value = 123;
+    android::base::unique_fd map(
+            android::bpf::createMap(BPF_MAP_TYPE_ARRAY, sizeof(key), sizeof(value), 2, 0));
+    if (android::bpf::writeToMapEntry(map, &key, &value, BPF_ANY)) {
+        ALOGE("Critical kernel bug - failure to write into index 1 of 2 element bpf map array.");
+        return 1;
+    }
+
+    if (android::base::SetProperty("bpf.progs_loaded", "1") == false) {
+        ALOGE("Failed to set bpf.progs_loaded property");
+        return 1;
+    }
+
+    return 0;
+}
diff --git a/netbpfload/loader.cpp b/netbpfload/loader.cpp
new file mode 100644
index 0000000..c534b2c
--- /dev/null
+++ b/netbpfload/loader.cpp
@@ -0,0 +1,1185 @@
+/*
+ * Copyright (C) 2018-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.
+ */
+
+#define LOG_TAG "NetBpfLoader"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/bpf.h>
+#include <linux/elf.h>
+#include <log/log.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sysexits.h>
+#include <sys/stat.h>
+#include <sys/utsname.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+// This is BpfLoader v0.41
+// WARNING: If you ever hit cherrypick conflicts here you're doing it wrong:
+// You are NOT allowed to cherrypick bpfloader related patches out of order.
+// (indeed: cherrypicking is probably a bad idea and you should merge instead)
+// Mainline supports ONLY the published versions of the bpfloader for each Android release.
+#define BPFLOADER_VERSION_MAJOR 0u
+#define BPFLOADER_VERSION_MINOR 41u
+#define BPFLOADER_VERSION ((BPFLOADER_VERSION_MAJOR << 16) | BPFLOADER_VERSION_MINOR)
+
+#include "BpfSyscallWrappers.h"
+#include "bpf/BpfUtils.h"
+#include "bpf/bpf_map_def.h"
+#include "loader.h"
+
+#if BPFLOADER_VERSION < COMPILE_FOR_BPFLOADER_VERSION
+#error "BPFLOADER_VERSION is less than COMPILE_FOR_BPFLOADER_VERSION"
+#endif
+
+#include <cstdlib>
+#include <fstream>
+#include <iostream>
+#include <optional>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include <android-base/cmsg.h>
+#include <android-base/file.h>
+#include <android-base/properties.h>
+#include <android-base/strings.h>
+#include <android-base/unique_fd.h>
+
+#define BPF_FS_PATH "/sys/fs/bpf/"
+
+// Size of the BPF log buffer for verifier logging
+#define BPF_LOAD_LOG_SZ 0xfffff
+
+// Unspecified attach type is 0 which is BPF_CGROUP_INET_INGRESS.
+#define BPF_ATTACH_TYPE_UNSPEC BPF_CGROUP_INET_INGRESS
+
+using android::base::StartsWith;
+using android::base::unique_fd;
+using std::ifstream;
+using std::ios;
+using std::optional;
+using std::string;
+using std::vector;
+
+namespace android {
+namespace bpf {
+
+const std::string& getBuildType() {
+    static std::string t = android::base::GetProperty("ro.build.type", "unknown");
+    return t;
+}
+
+static unsigned int page_size = static_cast<unsigned int>(getpagesize());
+
+constexpr const char* lookupSelinuxContext(const domain d, const char* const unspecified = "") {
+    switch (d) {
+        case domain::unspecified:   return unspecified;
+        case domain::tethering:     return "fs_bpf_tethering";
+        case domain::net_private:   return "fs_bpf_net_private";
+        case domain::net_shared:    return "fs_bpf_net_shared";
+        case domain::netd_readonly: return "fs_bpf_netd_readonly";
+        case domain::netd_shared:   return "fs_bpf_netd_shared";
+        default:                    return "(unrecognized)";
+    }
+}
+
+domain getDomainFromSelinuxContext(const char s[BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE]) {
+    for (domain d : AllDomains) {
+        // Not sure how to enforce this at compile time, so abort() bpfloader at boot instead
+        if (strlen(lookupSelinuxContext(d)) >= BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE) abort();
+        if (!strncmp(s, lookupSelinuxContext(d), BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE)) return d;
+    }
+    ALOGW("ignoring unrecognized selinux_context '%-32s'", s);
+    // We should return 'unrecognized' here, however: returning unspecified will
+    // result in the system simply using the default context, which in turn
+    // will allow future expansion by adding more restrictive selinux types.
+    // Older bpfloader will simply ignore that, and use the less restrictive default.
+    // This does mean you CANNOT later add a *less* restrictive type than the default.
+    //
+    // Note: we cannot just abort() here as this might be a mainline module shipped optional update
+    return domain::unspecified;
+}
+
+constexpr const char* lookupPinSubdir(const domain d, const char* const unspecified = "") {
+    switch (d) {
+        case domain::unspecified:   return unspecified;
+        case domain::tethering:     return "tethering/";
+        case domain::net_private:   return "net_private/";
+        case domain::net_shared:    return "net_shared/";
+        case domain::netd_readonly: return "netd_readonly/";
+        case domain::netd_shared:   return "netd_shared/";
+        default:                    return "(unrecognized)";
+    }
+};
+
+domain getDomainFromPinSubdir(const char s[BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE]) {
+    for (domain d : AllDomains) {
+        // Not sure how to enforce this at compile time, so abort() bpfloader at boot instead
+        if (strlen(lookupPinSubdir(d)) >= BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE) abort();
+        if (!strncmp(s, lookupPinSubdir(d), BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE)) return d;
+    }
+    ALOGE("unrecognized pin_subdir '%-32s'", s);
+    // pin_subdir affects the object's full pathname,
+    // and thus using the default would change the location and thus our code's ability to find it,
+    // hence this seems worth treating as a true error condition.
+    //
+    // Note: we cannot just abort() here as this might be a mainline module shipped optional update
+    // However, our callers will treat this as an error, and stop loading the specific .o,
+    // which will fail bpfloader if the .o is marked critical.
+    return domain::unrecognized;
+}
+
+static string pathToObjName(const string& path) {
+    // extract everything after the final slash, ie. this is the filename 'foo@1.o' or 'bar.o'
+    string filename = android::base::Split(path, "/").back();
+    // strip off everything from the final period onwards (strip '.o' suffix), ie. 'foo@1' or 'bar'
+    string name = filename.substr(0, filename.find_last_of('.'));
+    // strip any potential @1 suffix, this will leave us with just 'foo' or 'bar'
+    // this can be used to provide duplicate programs (mux based on the bpfloader version)
+    return name.substr(0, name.find_last_of('@'));
+}
+
+typedef struct {
+    const char* name;
+    enum bpf_prog_type type;
+    enum bpf_attach_type expected_attach_type;
+} sectionType;
+
+/*
+ * Map section name prefixes to program types, the section name will be:
+ *   SECTION(<prefix>/<name-of-program>)
+ * For example:
+ *   SECTION("tracepoint/sched_switch_func") where sched_switch_funcs
+ * is the name of the program, and tracepoint is the type.
+ *
+ * However, be aware that you should not be directly using the SECTION() macro.
+ * Instead use the DEFINE_(BPF|XDP)_(PROG|MAP)... & LICENSE/CRITICAL macros.
+ *
+ * Programs shipped inside the tethering apex should be limited to networking stuff,
+ * as KPROBE, PERF_EVENT, TRACEPOINT are dangerous to use from mainline updatable code,
+ * since they are less stable abi/api and may conflict with platform uses of bpf.
+ */
+sectionType sectionNameTypes[] = {
+        {"bind4/",         BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET4_BIND},
+        {"bind6/",         BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET6_BIND},
+        {"cgroupskb/",     BPF_PROG_TYPE_CGROUP_SKB,       BPF_ATTACH_TYPE_UNSPEC},
+        {"cgroupsock/",    BPF_PROG_TYPE_CGROUP_SOCK,      BPF_ATTACH_TYPE_UNSPEC},
+        {"connect4/",      BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET4_CONNECT},
+        {"connect6/",      BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET6_CONNECT},
+        {"egress/",        BPF_PROG_TYPE_CGROUP_SKB,       BPF_CGROUP_INET_EGRESS},
+        {"getsockopt/",    BPF_PROG_TYPE_CGROUP_SOCKOPT,   BPF_CGROUP_GETSOCKOPT},
+        {"ingress/",       BPF_PROG_TYPE_CGROUP_SKB,       BPF_CGROUP_INET_INGRESS},
+        {"lwt_in/",        BPF_PROG_TYPE_LWT_IN,           BPF_ATTACH_TYPE_UNSPEC},
+        {"lwt_out/",       BPF_PROG_TYPE_LWT_OUT,          BPF_ATTACH_TYPE_UNSPEC},
+        {"lwt_seg6local/", BPF_PROG_TYPE_LWT_SEG6LOCAL,    BPF_ATTACH_TYPE_UNSPEC},
+        {"lwt_xmit/",      BPF_PROG_TYPE_LWT_XMIT,         BPF_ATTACH_TYPE_UNSPEC},
+        {"postbind4/",     BPF_PROG_TYPE_CGROUP_SOCK,      BPF_CGROUP_INET4_POST_BIND},
+        {"postbind6/",     BPF_PROG_TYPE_CGROUP_SOCK,      BPF_CGROUP_INET6_POST_BIND},
+        {"recvmsg4/",      BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP4_RECVMSG},
+        {"recvmsg6/",      BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP6_RECVMSG},
+        {"schedact/",      BPF_PROG_TYPE_SCHED_ACT,        BPF_ATTACH_TYPE_UNSPEC},
+        {"schedcls/",      BPF_PROG_TYPE_SCHED_CLS,        BPF_ATTACH_TYPE_UNSPEC},
+        {"sendmsg4/",      BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP4_SENDMSG},
+        {"sendmsg6/",      BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP6_SENDMSG},
+        {"setsockopt/",    BPF_PROG_TYPE_CGROUP_SOCKOPT,   BPF_CGROUP_SETSOCKOPT},
+        {"skfilter/",      BPF_PROG_TYPE_SOCKET_FILTER,    BPF_ATTACH_TYPE_UNSPEC},
+        {"sockops/",       BPF_PROG_TYPE_SOCK_OPS,         BPF_CGROUP_SOCK_OPS},
+        {"sysctl",         BPF_PROG_TYPE_CGROUP_SYSCTL,    BPF_CGROUP_SYSCTL},
+        {"xdp/",           BPF_PROG_TYPE_XDP,              BPF_ATTACH_TYPE_UNSPEC},
+};
+
+typedef struct {
+    enum bpf_prog_type type;
+    enum bpf_attach_type expected_attach_type;
+    string name;
+    vector<char> data;
+    vector<char> rel_data;
+    optional<struct bpf_prog_def> prog_def;
+
+    unique_fd prog_fd; /* fd after loading */
+} codeSection;
+
+static int readElfHeader(ifstream& elfFile, Elf64_Ehdr* eh) {
+    elfFile.seekg(0);
+    if (elfFile.fail()) return -1;
+
+    if (!elfFile.read((char*)eh, sizeof(*eh))) return -1;
+
+    return 0;
+}
+
+/* Reads all section header tables into an Shdr array */
+static int readSectionHeadersAll(ifstream& elfFile, vector<Elf64_Shdr>& shTable) {
+    Elf64_Ehdr eh;
+    int ret = 0;
+
+    ret = readElfHeader(elfFile, &eh);
+    if (ret) return ret;
+
+    elfFile.seekg(eh.e_shoff);
+    if (elfFile.fail()) return -1;
+
+    /* Read shdr table entries */
+    shTable.resize(eh.e_shnum);
+
+    if (!elfFile.read((char*)shTable.data(), (eh.e_shnum * eh.e_shentsize))) return -ENOMEM;
+
+    return 0;
+}
+
+/* Read a section by its index - for ex to get sec hdr strtab blob */
+static int readSectionByIdx(ifstream& elfFile, int id, vector<char>& sec) {
+    vector<Elf64_Shdr> shTable;
+    int ret = readSectionHeadersAll(elfFile, shTable);
+    if (ret) return ret;
+
+    elfFile.seekg(shTable[id].sh_offset);
+    if (elfFile.fail()) return -1;
+
+    sec.resize(shTable[id].sh_size);
+    if (!elfFile.read(sec.data(), shTable[id].sh_size)) return -1;
+
+    return 0;
+}
+
+/* Read whole section header string table */
+static int readSectionHeaderStrtab(ifstream& elfFile, vector<char>& strtab) {
+    Elf64_Ehdr eh;
+    int ret = readElfHeader(elfFile, &eh);
+    if (ret) return ret;
+
+    ret = readSectionByIdx(elfFile, eh.e_shstrndx, strtab);
+    if (ret) return ret;
+
+    return 0;
+}
+
+/* Get name from offset in strtab */
+static int getSymName(ifstream& elfFile, int nameOff, string& name) {
+    int ret;
+    vector<char> secStrTab;
+
+    ret = readSectionHeaderStrtab(elfFile, secStrTab);
+    if (ret) return ret;
+
+    if (nameOff >= (int)secStrTab.size()) return -1;
+
+    name = string((char*)secStrTab.data() + nameOff);
+    return 0;
+}
+
+/* Reads a full section by name - example to get the GPL license */
+static int readSectionByName(const char* name, ifstream& elfFile, vector<char>& data) {
+    vector<char> secStrTab;
+    vector<Elf64_Shdr> shTable;
+    int ret;
+
+    ret = readSectionHeadersAll(elfFile, shTable);
+    if (ret) return ret;
+
+    ret = readSectionHeaderStrtab(elfFile, secStrTab);
+    if (ret) return ret;
+
+    for (int i = 0; i < (int)shTable.size(); i++) {
+        char* secname = secStrTab.data() + shTable[i].sh_name;
+        if (!secname) continue;
+
+        if (!strcmp(secname, name)) {
+            vector<char> dataTmp;
+            dataTmp.resize(shTable[i].sh_size);
+
+            elfFile.seekg(shTable[i].sh_offset);
+            if (elfFile.fail()) return -1;
+
+            if (!elfFile.read((char*)dataTmp.data(), shTable[i].sh_size)) return -1;
+
+            data = dataTmp;
+            return 0;
+        }
+    }
+    return -2;
+}
+
+unsigned int readSectionUint(const char* name, ifstream& elfFile, unsigned int defVal) {
+    vector<char> theBytes;
+    int ret = readSectionByName(name, elfFile, theBytes);
+    if (ret) {
+        ALOGD("Couldn't find section %s (defaulting to %u [0x%x]).", name, defVal, defVal);
+        return defVal;
+    } else if (theBytes.size() < sizeof(unsigned int)) {
+        ALOGE("Section %s too short (defaulting to %u [0x%x]).", name, defVal, defVal);
+        return defVal;
+    } else {
+        // decode first 4 bytes as LE32 uint, there will likely be more bytes due to alignment.
+        unsigned int value = static_cast<unsigned char>(theBytes[3]);
+        value <<= 8;
+        value += static_cast<unsigned char>(theBytes[2]);
+        value <<= 8;
+        value += static_cast<unsigned char>(theBytes[1]);
+        value <<= 8;
+        value += static_cast<unsigned char>(theBytes[0]);
+        ALOGI("Section %s value is %u [0x%x]", name, value, value);
+        return value;
+    }
+}
+
+static int readSectionByType(ifstream& elfFile, int type, vector<char>& data) {
+    int ret;
+    vector<Elf64_Shdr> shTable;
+
+    ret = readSectionHeadersAll(elfFile, shTable);
+    if (ret) return ret;
+
+    for (int i = 0; i < (int)shTable.size(); i++) {
+        if ((int)shTable[i].sh_type != type) continue;
+
+        vector<char> dataTmp;
+        dataTmp.resize(shTable[i].sh_size);
+
+        elfFile.seekg(shTable[i].sh_offset);
+        if (elfFile.fail()) return -1;
+
+        if (!elfFile.read((char*)dataTmp.data(), shTable[i].sh_size)) return -1;
+
+        data = dataTmp;
+        return 0;
+    }
+    return -2;
+}
+
+static bool symCompare(Elf64_Sym a, Elf64_Sym b) {
+    return (a.st_value < b.st_value);
+}
+
+static int readSymTab(ifstream& elfFile, int sort, vector<Elf64_Sym>& data) {
+    int ret, numElems;
+    Elf64_Sym* buf;
+    vector<char> secData;
+
+    ret = readSectionByType(elfFile, SHT_SYMTAB, secData);
+    if (ret) return ret;
+
+    buf = (Elf64_Sym*)secData.data();
+    numElems = (secData.size() / sizeof(Elf64_Sym));
+    data.assign(buf, buf + numElems);
+
+    if (sort) std::sort(data.begin(), data.end(), symCompare);
+    return 0;
+}
+
+static enum bpf_prog_type getSectionType(string& name) {
+    for (auto& snt : sectionNameTypes)
+        if (StartsWith(name, snt.name)) return snt.type;
+
+    return BPF_PROG_TYPE_UNSPEC;
+}
+
+static enum bpf_attach_type getExpectedAttachType(string& name) {
+    for (auto& snt : sectionNameTypes)
+        if (StartsWith(name, snt.name)) return snt.expected_attach_type;
+    return BPF_ATTACH_TYPE_UNSPEC;
+}
+
+/*
+static string getSectionName(enum bpf_prog_type type)
+{
+    for (auto& snt : sectionNameTypes)
+        if (snt.type == type)
+            return string(snt.name);
+
+    return "UNKNOWN SECTION NAME " + std::to_string(type);
+}
+*/
+
+static int readProgDefs(ifstream& elfFile, vector<struct bpf_prog_def>& pd,
+                        size_t sizeOfBpfProgDef) {
+    vector<char> pdData;
+    int ret = readSectionByName("progs", elfFile, pdData);
+    // Older file formats do not require a 'progs' section at all.
+    // (We should probably figure out whether this is behaviour which is safe to remove now.)
+    if (ret == -2) return 0;
+    if (ret) return ret;
+
+    if (pdData.size() % sizeOfBpfProgDef) {
+        ALOGE("readProgDefs failed due to improper sized progs section, %zu %% %zu != 0",
+              pdData.size(), sizeOfBpfProgDef);
+        return -1;
+    };
+
+    int progCount = pdData.size() / sizeOfBpfProgDef;
+    pd.resize(progCount);
+    size_t trimmedSize = std::min(sizeOfBpfProgDef, sizeof(struct bpf_prog_def));
+
+    const char* dataPtr = pdData.data();
+    for (auto& p : pd) {
+        // First we zero initialize
+        memset(&p, 0, sizeof(p));
+        // Then we set non-zero defaults
+        p.bpfloader_max_ver = DEFAULT_BPFLOADER_MAX_VER;  // v1.0
+        // Then we copy over the structure prefix from the ELF file.
+        memcpy(&p, dataPtr, trimmedSize);
+        // Move to next struct in the ELF file
+        dataPtr += sizeOfBpfProgDef;
+    }
+    return 0;
+}
+
+static int getSectionSymNames(ifstream& elfFile, const string& sectionName, vector<string>& names,
+                              optional<unsigned> symbolType = std::nullopt) {
+    int ret;
+    string name;
+    vector<Elf64_Sym> symtab;
+    vector<Elf64_Shdr> shTable;
+
+    ret = readSymTab(elfFile, 1 /* sort */, symtab);
+    if (ret) return ret;
+
+    /* Get index of section */
+    ret = readSectionHeadersAll(elfFile, shTable);
+    if (ret) return ret;
+
+    int sec_idx = -1;
+    for (int i = 0; i < (int)shTable.size(); i++) {
+        ret = getSymName(elfFile, shTable[i].sh_name, name);
+        if (ret) return ret;
+
+        if (!name.compare(sectionName)) {
+            sec_idx = i;
+            break;
+        }
+    }
+
+    /* No section found with matching name*/
+    if (sec_idx == -1) {
+        ALOGW("No %s section could be found in elf object", sectionName.c_str());
+        return -1;
+    }
+
+    for (int i = 0; i < (int)symtab.size(); i++) {
+        if (symbolType.has_value() && ELF_ST_TYPE(symtab[i].st_info) != symbolType) continue;
+
+        if (symtab[i].st_shndx == sec_idx) {
+            string s;
+            ret = getSymName(elfFile, symtab[i].st_name, s);
+            if (ret) return ret;
+            names.push_back(s);
+        }
+    }
+
+    return 0;
+}
+
+/* Read a section by its index - for ex to get sec hdr strtab blob */
+static int readCodeSections(ifstream& elfFile, vector<codeSection>& cs, size_t sizeOfBpfProgDef) {
+    vector<Elf64_Shdr> shTable;
+    int entries, ret = 0;
+
+    ret = readSectionHeadersAll(elfFile, shTable);
+    if (ret) return ret;
+    entries = shTable.size();
+
+    vector<struct bpf_prog_def> pd;
+    ret = readProgDefs(elfFile, pd, sizeOfBpfProgDef);
+    if (ret) return ret;
+    vector<string> progDefNames;
+    ret = getSectionSymNames(elfFile, "progs", progDefNames);
+    if (!pd.empty() && ret) return ret;
+
+    for (int i = 0; i < entries; i++) {
+        string name;
+        codeSection cs_temp;
+        cs_temp.type = BPF_PROG_TYPE_UNSPEC;
+
+        ret = getSymName(elfFile, shTable[i].sh_name, name);
+        if (ret) return ret;
+
+        enum bpf_prog_type ptype = getSectionType(name);
+
+        if (ptype == BPF_PROG_TYPE_UNSPEC) continue;
+
+        // This must be done before '/' is replaced with '_'.
+        cs_temp.expected_attach_type = getExpectedAttachType(name);
+
+        string oldName = name;
+
+        // convert all slashes to underscores
+        std::replace(name.begin(), name.end(), '/', '_');
+
+        cs_temp.type = ptype;
+        cs_temp.name = name;
+
+        ret = readSectionByIdx(elfFile, i, cs_temp.data);
+        if (ret) return ret;
+        ALOGD("Loaded code section %d (%s)", i, name.c_str());
+
+        vector<string> csSymNames;
+        ret = getSectionSymNames(elfFile, oldName, csSymNames, STT_FUNC);
+        if (ret || !csSymNames.size()) return ret;
+        for (size_t i = 0; i < progDefNames.size(); ++i) {
+            if (!progDefNames[i].compare(csSymNames[0] + "_def")) {
+                cs_temp.prog_def = pd[i];
+                break;
+            }
+        }
+
+        /* Check for rel section */
+        if (cs_temp.data.size() > 0 && i < entries) {
+            ret = getSymName(elfFile, shTable[i + 1].sh_name, name);
+            if (ret) return ret;
+
+            if (name == (".rel" + oldName)) {
+                ret = readSectionByIdx(elfFile, i + 1, cs_temp.rel_data);
+                if (ret) return ret;
+                ALOGD("Loaded relo section %d (%s)", i, name.c_str());
+            }
+        }
+
+        if (cs_temp.data.size() > 0) {
+            cs.push_back(std::move(cs_temp));
+            ALOGD("Adding section %d to cs list", i);
+        }
+    }
+    return 0;
+}
+
+static int getSymNameByIdx(ifstream& elfFile, int index, string& name) {
+    vector<Elf64_Sym> symtab;
+    int ret = 0;
+
+    ret = readSymTab(elfFile, 0 /* !sort */, symtab);
+    if (ret) return ret;
+
+    if (index >= (int)symtab.size()) return -1;
+
+    return getSymName(elfFile, symtab[index].st_name, name);
+}
+
+static bool mapMatchesExpectations(const unique_fd& fd, const string& mapName,
+                                   const struct bpf_map_def& mapDef, const enum bpf_map_type type) {
+    // Assuming fd is a valid Bpf Map file descriptor then
+    // all the following should always succeed on a 4.14+ kernel.
+    // If they somehow do fail, they'll return -1 (and set errno),
+    // which should then cause (among others) a key_size mismatch.
+    int fd_type = bpfGetFdMapType(fd);
+    int fd_key_size = bpfGetFdKeySize(fd);
+    int fd_value_size = bpfGetFdValueSize(fd);
+    int fd_max_entries = bpfGetFdMaxEntries(fd);
+    int fd_map_flags = bpfGetFdMapFlags(fd);
+
+    // DEVMAPs are readonly from the bpf program side's point of view, as such
+    // the kernel in kernel/bpf/devmap.c dev_map_init_map() will set the flag
+    int desired_map_flags = (int)mapDef.map_flags;
+    if (type == BPF_MAP_TYPE_DEVMAP || type == BPF_MAP_TYPE_DEVMAP_HASH)
+        desired_map_flags |= BPF_F_RDONLY_PROG;
+
+    // The .h file enforces that this is a power of two, and page size will
+    // also always be a power of two, so this logic is actually enough to
+    // force it to be a multiple of the page size, as required by the kernel.
+    unsigned int desired_max_entries = mapDef.max_entries;
+    if (type == BPF_MAP_TYPE_RINGBUF) {
+        if (desired_max_entries < page_size) desired_max_entries = page_size;
+    }
+
+    // The following checks should *never* trigger, if one of them somehow does,
+    // it probably means a bpf .o file has been changed/replaced at runtime
+    // and bpfloader was manually rerun (normally it should only run *once*
+    // early during the boot process).
+    // Another possibility is that something is misconfigured in the code:
+    // most likely a shared map is declared twice differently.
+    // But such a change should never be checked into the source tree...
+    if ((fd_type == type) &&
+        (fd_key_size == (int)mapDef.key_size) &&
+        (fd_value_size == (int)mapDef.value_size) &&
+        (fd_max_entries == (int)desired_max_entries) &&
+        (fd_map_flags == desired_map_flags)) {
+        return true;
+    }
+
+    ALOGE("bpf map name %s mismatch: desired/found: "
+          "type:%d/%d key:%u/%d value:%u/%d entries:%u/%d flags:%u/%d",
+          mapName.c_str(), type, fd_type, mapDef.key_size, fd_key_size, mapDef.value_size,
+          fd_value_size, mapDef.max_entries, fd_max_entries, desired_map_flags, fd_map_flags);
+    return false;
+}
+
+static int createMaps(const char* elfPath, ifstream& elfFile, vector<unique_fd>& mapFds,
+                      const char* prefix, const size_t sizeOfBpfMapDef) {
+    int ret;
+    vector<char> mdData;
+    vector<struct bpf_map_def> md;
+    vector<string> mapNames;
+    string objName = pathToObjName(string(elfPath));
+
+    ret = readSectionByName("maps", elfFile, mdData);
+    if (ret == -2) return 0;  // no maps to read
+    if (ret) return ret;
+
+    if (mdData.size() % sizeOfBpfMapDef) {
+        ALOGE("createMaps failed due to improper sized maps section, %zu %% %zu != 0",
+              mdData.size(), sizeOfBpfMapDef);
+        return -1;
+    };
+
+    int mapCount = mdData.size() / sizeOfBpfMapDef;
+    md.resize(mapCount);
+    size_t trimmedSize = std::min(sizeOfBpfMapDef, sizeof(struct bpf_map_def));
+
+    const char* dataPtr = mdData.data();
+    for (auto& m : md) {
+        // First we zero initialize
+        memset(&m, 0, sizeof(m));
+        // Then we set non-zero defaults
+        m.bpfloader_max_ver = DEFAULT_BPFLOADER_MAX_VER;  // v1.0
+        m.max_kver = 0xFFFFFFFFu;                         // matches KVER_INF from bpf_helpers.h
+        // Then we copy over the structure prefix from the ELF file.
+        memcpy(&m, dataPtr, trimmedSize);
+        // Move to next struct in the ELF file
+        dataPtr += sizeOfBpfMapDef;
+    }
+
+    ret = getSectionSymNames(elfFile, "maps", mapNames);
+    if (ret) return ret;
+
+    unsigned kvers = kernelVersion();
+
+    for (int i = 0; i < (int)mapNames.size(); i++) {
+        if (md[i].zero != 0) abort();
+
+        if (BPFLOADER_VERSION < md[i].bpfloader_min_ver) {
+            ALOGI("skipping map %s which requires bpfloader min ver 0x%05x", mapNames[i].c_str(),
+                  md[i].bpfloader_min_ver);
+            mapFds.push_back(unique_fd());
+            continue;
+        }
+
+        if (BPFLOADER_VERSION >= md[i].bpfloader_max_ver) {
+            ALOGI("skipping map %s which requires bpfloader max ver 0x%05x", mapNames[i].c_str(),
+                  md[i].bpfloader_max_ver);
+            mapFds.push_back(unique_fd());
+            continue;
+        }
+
+        if (kvers < md[i].min_kver) {
+            ALOGI("skipping map %s which requires kernel version 0x%x >= 0x%x",
+                  mapNames[i].c_str(), kvers, md[i].min_kver);
+            mapFds.push_back(unique_fd());
+            continue;
+        }
+
+        if (kvers >= md[i].max_kver) {
+            ALOGI("skipping map %s which requires kernel version 0x%x < 0x%x",
+                  mapNames[i].c_str(), kvers, md[i].max_kver);
+            mapFds.push_back(unique_fd());
+            continue;
+        }
+
+        if ((md[i].ignore_on_eng && isEng()) || (md[i].ignore_on_user && isUser()) ||
+            (md[i].ignore_on_userdebug && isUserdebug())) {
+            ALOGI("skipping map %s which is ignored on %s builds", mapNames[i].c_str(),
+                  getBuildType().c_str());
+            mapFds.push_back(unique_fd());
+            continue;
+        }
+
+        if ((isArm() && isKernel32Bit() && md[i].ignore_on_arm32) ||
+            (isArm() && isKernel64Bit() && md[i].ignore_on_aarch64) ||
+            (isX86() && isKernel32Bit() && md[i].ignore_on_x86_32) ||
+            (isX86() && isKernel64Bit() && md[i].ignore_on_x86_64) ||
+            (isRiscV() && md[i].ignore_on_riscv64)) {
+            ALOGI("skipping map %s which is ignored on %s", mapNames[i].c_str(),
+                  describeArch());
+            mapFds.push_back(unique_fd());
+            continue;
+        }
+
+        enum bpf_map_type type = md[i].type;
+        if (type == BPF_MAP_TYPE_DEVMAP_HASH && !isAtLeastKernelVersion(5, 4, 0)) {
+            // On Linux Kernels older than 5.4 this map type doesn't exist, but it can kind
+            // of be approximated: HASH has the same userspace visible api.
+            // However it cannot be used by ebpf programs in the same way.
+            // Since bpf_redirect_map() only requires 4.14, a program using a DEVMAP_HASH map
+            // would fail to load (due to trying to redirect to a HASH instead of DEVMAP_HASH).
+            // One must thus tag any BPF_MAP_TYPE_DEVMAP_HASH + bpf_redirect_map() using
+            // programs as being 5.4+...
+            type = BPF_MAP_TYPE_HASH;
+        }
+
+        // The .h file enforces that this is a power of two, and page size will
+        // also always be a power of two, so this logic is actually enough to
+        // force it to be a multiple of the page size, as required by the kernel.
+        unsigned int max_entries = md[i].max_entries;
+        if (type == BPF_MAP_TYPE_RINGBUF) {
+            if (max_entries < page_size) max_entries = page_size;
+        }
+
+        domain selinux_context = getDomainFromSelinuxContext(md[i].selinux_context);
+        if (specified(selinux_context)) {
+            ALOGI("map %s selinux_context [%-32s] -> %d -> '%s' (%s)", mapNames[i].c_str(),
+                  md[i].selinux_context, selinux_context, lookupSelinuxContext(selinux_context),
+                  lookupPinSubdir(selinux_context));
+        }
+
+        domain pin_subdir = getDomainFromPinSubdir(md[i].pin_subdir);
+        if (unrecognized(pin_subdir)) return -ENOTDIR;
+        if (specified(pin_subdir)) {
+            ALOGI("map %s pin_subdir [%-32s] -> %d -> '%s'", mapNames[i].c_str(), md[i].pin_subdir,
+                  pin_subdir, lookupPinSubdir(pin_subdir));
+        }
+
+        // Format of pin location is /sys/fs/bpf/<pin_subdir|prefix>map_<objName>_<mapName>
+        // except that maps shared across .o's have empty <objName>
+        // Note: <objName> refers to the extension-less basename of the .o file (without @ suffix).
+        string mapPinLoc = string(BPF_FS_PATH) + lookupPinSubdir(pin_subdir, prefix) + "map_" +
+                           (md[i].shared ? "" : objName) + "_" + mapNames[i];
+        bool reuse = false;
+        unique_fd fd;
+        int saved_errno;
+
+        if (access(mapPinLoc.c_str(), F_OK) == 0) {
+            fd.reset(mapRetrieveRO(mapPinLoc.c_str()));
+            saved_errno = errno;
+            ALOGD("bpf_create_map reusing map %s, ret: %d", mapNames[i].c_str(), fd.get());
+            reuse = true;
+        } else {
+            union bpf_attr req = {
+              .map_type = type,
+              .key_size = md[i].key_size,
+              .value_size = md[i].value_size,
+              .max_entries = max_entries,
+              .map_flags = md[i].map_flags,
+            };
+            strlcpy(req.map_name, mapNames[i].c_str(), sizeof(req.map_name));
+            fd.reset(bpf(BPF_MAP_CREATE, req));
+            saved_errno = errno;
+            ALOGD("bpf_create_map name %s, ret: %d", mapNames[i].c_str(), fd.get());
+        }
+
+        if (!fd.ok()) return -saved_errno;
+
+        // When reusing a pinned map, we need to check the map type/sizes/etc match, but for
+        // safety (since reuse code path is rare) run these checks even if we just created it.
+        // We assume failure is due to pinned map mismatch, hence the 'NOT UNIQUE' return code.
+        if (!mapMatchesExpectations(fd, mapNames[i], md[i], type)) return -ENOTUNIQ;
+
+        if (!reuse) {
+            if (specified(selinux_context)) {
+                string createLoc = string(BPF_FS_PATH) + lookupPinSubdir(selinux_context) +
+                                   "tmp_map_" + objName + "_" + mapNames[i];
+                ret = bpfFdPin(fd, createLoc.c_str());
+                if (ret) {
+                    int err = errno;
+                    ALOGE("create %s -> %d [%d:%s]", createLoc.c_str(), ret, err, strerror(err));
+                    return -err;
+                }
+                ret = renameat2(AT_FDCWD, createLoc.c_str(),
+                                AT_FDCWD, mapPinLoc.c_str(), RENAME_NOREPLACE);
+                if (ret) {
+                    int err = errno;
+                    ALOGE("rename %s %s -> %d [%d:%s]", createLoc.c_str(), mapPinLoc.c_str(), ret,
+                          err, strerror(err));
+                    return -err;
+                }
+            } else {
+                ret = bpfFdPin(fd, mapPinLoc.c_str());
+                if (ret) {
+                    int err = errno;
+                    ALOGE("pin %s -> %d [%d:%s]", mapPinLoc.c_str(), ret, err, strerror(err));
+                    return -err;
+                }
+            }
+            ret = chmod(mapPinLoc.c_str(), md[i].mode);
+            if (ret) {
+                int err = errno;
+                ALOGE("chmod(%s, 0%o) = %d [%d:%s]", mapPinLoc.c_str(), md[i].mode, ret, err,
+                      strerror(err));
+                return -err;
+            }
+            ret = chown(mapPinLoc.c_str(), (uid_t)md[i].uid, (gid_t)md[i].gid);
+            if (ret) {
+                int err = errno;
+                ALOGE("chown(%s, %u, %u) = %d [%d:%s]", mapPinLoc.c_str(), md[i].uid, md[i].gid,
+                      ret, err, strerror(err));
+                return -err;
+            }
+        }
+
+        int mapId = bpfGetFdMapId(fd);
+        if (mapId == -1) {
+            ALOGE("bpfGetFdMapId failed, ret: %d [%d]", mapId, errno);
+        } else {
+            ALOGI("map %s id %d", mapPinLoc.c_str(), mapId);
+        }
+
+        mapFds.push_back(std::move(fd));
+    }
+
+    return ret;
+}
+
+/* For debugging, dump all instructions */
+static void dumpIns(char* ins, int size) {
+    for (int row = 0; row < size / 8; row++) {
+        ALOGE("%d: ", row);
+        for (int j = 0; j < 8; j++) {
+            ALOGE("%3x ", ins[(row * 8) + j]);
+        }
+        ALOGE("\n");
+    }
+}
+
+/* For debugging, dump all code sections from cs list */
+static void dumpAllCs(vector<codeSection>& cs) {
+    for (int i = 0; i < (int)cs.size(); i++) {
+        ALOGE("Dumping cs %d, name %s", int(i), cs[i].name.c_str());
+        dumpIns((char*)cs[i].data.data(), cs[i].data.size());
+        ALOGE("-----------");
+    }
+}
+
+static void applyRelo(void* insnsPtr, Elf64_Addr offset, int fd) {
+    int insnIndex;
+    struct bpf_insn *insn, *insns;
+
+    insns = (struct bpf_insn*)(insnsPtr);
+
+    insnIndex = offset / sizeof(struct bpf_insn);
+    insn = &insns[insnIndex];
+
+    // Occasionally might be useful for relocation debugging, but pretty spammy
+    if (0) {
+        ALOGD("applying relo to instruction at byte offset: %llu, "
+              "insn offset %d, insn %llx",
+              (unsigned long long)offset, insnIndex, *(unsigned long long*)insn);
+    }
+
+    if (insn->code != (BPF_LD | BPF_IMM | BPF_DW)) {
+        ALOGE("Dumping all instructions till ins %d", insnIndex);
+        ALOGE("invalid relo for insn %d: code 0x%x", insnIndex, insn->code);
+        dumpIns((char*)insnsPtr, (insnIndex + 3) * 8);
+        return;
+    }
+
+    insn->imm = fd;
+    insn->src_reg = BPF_PSEUDO_MAP_FD;
+}
+
+static void applyMapRelo(ifstream& elfFile, vector<unique_fd> &mapFds, vector<codeSection>& cs) {
+    vector<string> mapNames;
+
+    int ret = getSectionSymNames(elfFile, "maps", mapNames);
+    if (ret) return;
+
+    for (int k = 0; k != (int)cs.size(); k++) {
+        Elf64_Rel* rel = (Elf64_Rel*)(cs[k].rel_data.data());
+        int n_rel = cs[k].rel_data.size() / sizeof(*rel);
+
+        for (int i = 0; i < n_rel; i++) {
+            int symIndex = ELF64_R_SYM(rel[i].r_info);
+            string symName;
+
+            ret = getSymNameByIdx(elfFile, symIndex, symName);
+            if (ret) return;
+
+            /* Find the map fd and apply relo */
+            for (int j = 0; j < (int)mapNames.size(); j++) {
+                if (!mapNames[j].compare(symName)) {
+                    applyRelo(cs[k].data.data(), rel[i].r_offset, mapFds[j]);
+                    break;
+                }
+            }
+        }
+    }
+}
+
+static int loadCodeSections(const char* elfPath, vector<codeSection>& cs, const string& license,
+                            const char* prefix) {
+    unsigned kvers = kernelVersion();
+
+    if (!kvers) {
+        ALOGE("unable to get kernel version");
+        return -EINVAL;
+    }
+
+    string objName = pathToObjName(string(elfPath));
+
+    for (int i = 0; i < (int)cs.size(); i++) {
+        unique_fd& fd = cs[i].prog_fd;
+        int ret;
+        string name = cs[i].name;
+
+        if (!cs[i].prog_def.has_value()) {
+            ALOGE("[%d] '%s' missing program definition! bad bpf.o build?", i, name.c_str());
+            return -EINVAL;
+        }
+
+        unsigned min_kver = cs[i].prog_def->min_kver;
+        unsigned max_kver = cs[i].prog_def->max_kver;
+        ALOGD("cs[%d].name:%s min_kver:%x .max_kver:%x (kvers:%x)", i, name.c_str(), min_kver,
+             max_kver, kvers);
+        if (kvers < min_kver) continue;
+        if (kvers >= max_kver) continue;
+
+        unsigned bpfMinVer = cs[i].prog_def->bpfloader_min_ver;
+        unsigned bpfMaxVer = cs[i].prog_def->bpfloader_max_ver;
+        domain selinux_context = getDomainFromSelinuxContext(cs[i].prog_def->selinux_context);
+        domain pin_subdir = getDomainFromPinSubdir(cs[i].prog_def->pin_subdir);
+        // Note: make sure to only check for unrecognized *after* verifying bpfloader
+        // version limits include this bpfloader's version.
+
+        ALOGD("cs[%d].name:%s requires bpfloader version [0x%05x,0x%05x)", i, name.c_str(),
+              bpfMinVer, bpfMaxVer);
+        if (BPFLOADER_VERSION < bpfMinVer) continue;
+        if (BPFLOADER_VERSION >= bpfMaxVer) continue;
+
+        if ((cs[i].prog_def->ignore_on_eng && isEng()) ||
+            (cs[i].prog_def->ignore_on_user && isUser()) ||
+            (cs[i].prog_def->ignore_on_userdebug && isUserdebug())) {
+            ALOGD("cs[%d].name:%s is ignored on %s builds", i, name.c_str(),
+                  getBuildType().c_str());
+            continue;
+        }
+
+        if ((isArm() && isKernel32Bit() && cs[i].prog_def->ignore_on_arm32) ||
+            (isArm() && isKernel64Bit() && cs[i].prog_def->ignore_on_aarch64) ||
+            (isX86() && isKernel32Bit() && cs[i].prog_def->ignore_on_x86_32) ||
+            (isX86() && isKernel64Bit() && cs[i].prog_def->ignore_on_x86_64) ||
+            (isRiscV() && cs[i].prog_def->ignore_on_riscv64)) {
+            ALOGD("cs[%d].name:%s is ignored on %s", i, name.c_str(), describeArch());
+            continue;
+        }
+
+        if (unrecognized(pin_subdir)) return -ENOTDIR;
+
+        if (specified(selinux_context)) {
+            ALOGI("prog %s selinux_context [%-32s] -> %d -> '%s' (%s)", name.c_str(),
+                  cs[i].prog_def->selinux_context, selinux_context,
+                  lookupSelinuxContext(selinux_context), lookupPinSubdir(selinux_context));
+        }
+
+        if (specified(pin_subdir)) {
+            ALOGI("prog %s pin_subdir [%-32s] -> %d -> '%s'", name.c_str(),
+                  cs[i].prog_def->pin_subdir, pin_subdir, lookupPinSubdir(pin_subdir));
+        }
+
+        // strip any potential $foo suffix
+        // this can be used to provide duplicate programs
+        // conditionally loaded based on running kernel version
+        name = name.substr(0, name.find_last_of('$'));
+
+        bool reuse = false;
+        // Format of pin location is
+        // /sys/fs/bpf/<prefix>prog_<objName>_<progName>
+        string progPinLoc = string(BPF_FS_PATH) + lookupPinSubdir(pin_subdir, prefix) + "prog_" +
+                            objName + '_' + string(name);
+        if (access(progPinLoc.c_str(), F_OK) == 0) {
+            fd.reset(retrieveProgram(progPinLoc.c_str()));
+            ALOGD("New bpf prog load reusing prog %s, ret: %d (%s)", progPinLoc.c_str(), fd.get(),
+                  (!fd.ok() ? std::strerror(errno) : "no error"));
+            reuse = true;
+        } else {
+            vector<char> log_buf(BPF_LOAD_LOG_SZ, 0);
+
+            union bpf_attr req = {
+              .prog_type = cs[i].type,
+              .kern_version = kvers,
+              .license = ptr_to_u64(license.c_str()),
+              .insns = ptr_to_u64(cs[i].data.data()),
+              .insn_cnt = static_cast<__u32>(cs[i].data.size() / sizeof(struct bpf_insn)),
+              .log_level = 1,
+              .log_buf = ptr_to_u64(log_buf.data()),
+              .log_size = static_cast<__u32>(log_buf.size()),
+              .expected_attach_type = cs[i].expected_attach_type,
+            };
+            strlcpy(req.prog_name, cs[i].name.c_str(), sizeof(req.prog_name));
+            fd.reset(bpf(BPF_PROG_LOAD, req));
+
+            ALOGD("BPF_PROG_LOAD call for %s (%s) returned fd: %d (%s)", elfPath,
+                  cs[i].name.c_str(), fd.get(), (!fd.ok() ? std::strerror(errno) : "no error"));
+
+            if (!fd.ok()) {
+                vector<string> lines = android::base::Split(log_buf.data(), "\n");
+
+                ALOGW("BPF_PROG_LOAD - BEGIN log_buf contents:");
+                for (const auto& line : lines) ALOGW("%s", line.c_str());
+                ALOGW("BPF_PROG_LOAD - END log_buf contents.");
+
+                if (cs[i].prog_def->optional) {
+                    ALOGW("failed program is marked optional - continuing...");
+                    continue;
+                }
+                ALOGE("non-optional program failed to load.");
+            }
+        }
+
+        if (!fd.ok()) return fd.get();
+
+        if (!reuse) {
+            if (specified(selinux_context)) {
+                string createLoc = string(BPF_FS_PATH) + lookupPinSubdir(selinux_context) +
+                                   "tmp_prog_" + objName + '_' + string(name);
+                ret = bpfFdPin(fd, createLoc.c_str());
+                if (ret) {
+                    int err = errno;
+                    ALOGE("create %s -> %d [%d:%s]", createLoc.c_str(), ret, err, strerror(err));
+                    return -err;
+                }
+                ret = renameat2(AT_FDCWD, createLoc.c_str(),
+                                AT_FDCWD, progPinLoc.c_str(), RENAME_NOREPLACE);
+                if (ret) {
+                    int err = errno;
+                    ALOGE("rename %s %s -> %d [%d:%s]", createLoc.c_str(), progPinLoc.c_str(), ret,
+                          err, strerror(err));
+                    return -err;
+                }
+            } else {
+                ret = bpfFdPin(fd, progPinLoc.c_str());
+                if (ret) {
+                    int err = errno;
+                    ALOGE("create %s -> %d [%d:%s]", progPinLoc.c_str(), ret, err, strerror(err));
+                    return -err;
+                }
+            }
+            if (chmod(progPinLoc.c_str(), 0440)) {
+                int err = errno;
+                ALOGE("chmod %s 0440 -> [%d:%s]", progPinLoc.c_str(), err, strerror(err));
+                return -err;
+            }
+            if (chown(progPinLoc.c_str(), (uid_t)cs[i].prog_def->uid,
+                      (gid_t)cs[i].prog_def->gid)) {
+                int err = errno;
+                ALOGE("chown %s %d %d -> [%d:%s]", progPinLoc.c_str(), cs[i].prog_def->uid,
+                      cs[i].prog_def->gid, err, strerror(err));
+                return -err;
+            }
+        }
+
+        int progId = bpfGetFdProgId(fd);
+        if (progId == -1) {
+            ALOGE("bpfGetFdProgId failed, ret: %d [%d]", progId, errno);
+        } else {
+            ALOGI("prog %s id %d", progPinLoc.c_str(), progId);
+        }
+    }
+
+    return 0;
+}
+
+int loadProg(const char* elfPath, bool* isCritical, const Location& location) {
+    vector<char> license;
+    vector<char> critical;
+    vector<codeSection> cs;
+    vector<unique_fd> mapFds;
+    int ret;
+
+    if (!isCritical) return -1;
+    *isCritical = false;
+
+    ifstream elfFile(elfPath, ios::in | ios::binary);
+    if (!elfFile.is_open()) return -1;
+
+    ret = readSectionByName("critical", elfFile, critical);
+    *isCritical = !ret;
+
+    ret = readSectionByName("license", elfFile, license);
+    if (ret) {
+        ALOGE("Couldn't find license in %s", elfPath);
+        return ret;
+    } else {
+        ALOGD("Loading %s%s ELF object %s with license %s",
+              *isCritical ? "critical for " : "optional", *isCritical ? (char*)critical.data() : "",
+              elfPath, (char*)license.data());
+    }
+
+    // the following default values are for bpfloader V0.0 format which does not include them
+    unsigned int bpfLoaderMinVer =
+            readSectionUint("bpfloader_min_ver", elfFile, DEFAULT_BPFLOADER_MIN_VER);
+    unsigned int bpfLoaderMaxVer =
+            readSectionUint("bpfloader_max_ver", elfFile, DEFAULT_BPFLOADER_MAX_VER);
+    unsigned int bpfLoaderMinRequiredVer =
+            readSectionUint("bpfloader_min_required_ver", elfFile, 0);
+    size_t sizeOfBpfMapDef =
+            readSectionUint("size_of_bpf_map_def", elfFile, DEFAULT_SIZEOF_BPF_MAP_DEF);
+    size_t sizeOfBpfProgDef =
+            readSectionUint("size_of_bpf_prog_def", elfFile, DEFAULT_SIZEOF_BPF_PROG_DEF);
+
+    // inclusive lower bound check
+    if (BPFLOADER_VERSION < bpfLoaderMinVer) {
+        ALOGI("BpfLoader version 0x%05x ignoring ELF object %s with min ver 0x%05x",
+              BPFLOADER_VERSION, elfPath, bpfLoaderMinVer);
+        return 0;
+    }
+
+    // exclusive upper bound check
+    if (BPFLOADER_VERSION >= bpfLoaderMaxVer) {
+        ALOGI("BpfLoader version 0x%05x ignoring ELF object %s with max ver 0x%05x",
+              BPFLOADER_VERSION, elfPath, bpfLoaderMaxVer);
+        return 0;
+    }
+
+    if (BPFLOADER_VERSION < bpfLoaderMinRequiredVer) {
+        ALOGI("BpfLoader version 0x%05x failing due to ELF object %s with required min ver 0x%05x",
+              BPFLOADER_VERSION, elfPath, bpfLoaderMinRequiredVer);
+        return -1;
+    }
+
+    ALOGI("BpfLoader version 0x%05x processing ELF object %s with ver [0x%05x,0x%05x)",
+          BPFLOADER_VERSION, elfPath, bpfLoaderMinVer, bpfLoaderMaxVer);
+
+    if (sizeOfBpfMapDef < DEFAULT_SIZEOF_BPF_MAP_DEF) {
+        ALOGE("sizeof(bpf_map_def) of %zu is too small (< %d)", sizeOfBpfMapDef,
+              DEFAULT_SIZEOF_BPF_MAP_DEF);
+        return -1;
+    }
+
+    if (sizeOfBpfProgDef < DEFAULT_SIZEOF_BPF_PROG_DEF) {
+        ALOGE("sizeof(bpf_prog_def) of %zu is too small (< %d)", sizeOfBpfProgDef,
+              DEFAULT_SIZEOF_BPF_PROG_DEF);
+        return -1;
+    }
+
+    ret = readCodeSections(elfFile, cs, sizeOfBpfProgDef);
+    if (ret) {
+        ALOGE("Couldn't read all code sections in %s", elfPath);
+        return ret;
+    }
+
+    /* Just for future debugging */
+    if (0) dumpAllCs(cs);
+
+    ret = createMaps(elfPath, elfFile, mapFds, location.prefix, sizeOfBpfMapDef);
+    if (ret) {
+        ALOGE("Failed to create maps: (ret=%d) in %s", ret, elfPath);
+        return ret;
+    }
+
+    for (int i = 0; i < (int)mapFds.size(); i++)
+        ALOGD("map_fd found at %d is %d in %s", i, mapFds[i].get(), elfPath);
+
+    applyMapRelo(elfFile, mapFds, cs);
+
+    ret = loadCodeSections(elfPath, cs, string(license.data()), location.prefix);
+    if (ret) ALOGE("Failed to load programs, loadCodeSections ret=%d", ret);
+
+    return ret;
+}
+
+}  // namespace bpf
+}  // namespace android
diff --git a/netbpfload/loader.h b/netbpfload/loader.h
new file mode 100644
index 0000000..b884637
--- /dev/null
+++ b/netbpfload/loader.h
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2018-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.
+ */
+
+#pragma once
+
+#include <linux/bpf.h>
+
+#include <fstream>
+
+namespace android {
+namespace bpf {
+
+// Bpf programs may specify per-program & per-map selinux_context and pin_subdir.
+//
+// The BpfLoader needs to convert these bpf.o specified strings into an enum
+// for internal use (to check that valid values were specified for the specific
+// location of the bpf.o file).
+//
+// It also needs to map selinux_context's into pin_subdir's.
+// This is because of how selinux_context is actually implemented via pin+rename.
+//
+// Thus 'domain' enumerates all selinux_context's/pin_subdir's that the BpfLoader
+// is aware of.  Thus there currently needs to be a 1:1 mapping between the two.
+//
+enum class domain : int {
+    unrecognized = -1,  // invalid for this version of the bpfloader
+    unspecified = 0,    // means just use the default for that specific pin location
+    tethering,          // (S+) fs_bpf_tethering     /sys/fs/bpf/tethering
+    net_private,        // (T+) fs_bpf_net_private   /sys/fs/bpf/net_private
+    net_shared,         // (T+) fs_bpf_net_shared    /sys/fs/bpf/net_shared
+    netd_readonly,      // (T+) fs_bpf_netd_readonly /sys/fs/bpf/netd_readonly
+    netd_shared,        // (T+) fs_bpf_netd_shared   /sys/fs/bpf/netd_shared
+};
+
+// Note: this does not include domain::unrecognized, but does include domain::unspecified
+static constexpr domain AllDomains[] = {
+    domain::unspecified,
+    domain::tethering,
+    domain::net_private,
+    domain::net_shared,
+    domain::netd_readonly,
+    domain::netd_shared,
+};
+
+static constexpr bool unrecognized(domain d) {
+    return d == domain::unrecognized;
+}
+
+// Note: this doesn't handle unrecognized, handle it first.
+static constexpr bool specified(domain d) {
+    return d != domain::unspecified;
+}
+
+struct Location {
+    const char* const dir = "";
+    const char* const prefix = "";
+};
+
+// BPF loader implementation. Loads an eBPF ELF object
+int loadProg(const char* elfPath, bool* isCritical, const Location &location = {});
+
+// Exposed for testing
+unsigned int readSectionUint(const char* name, std::ifstream& elfFile, unsigned int defVal);
+
+// Returns the build type string (from ro.build.type).
+const std::string& getBuildType();
+
+// The following functions classify the 3 Android build types.
+inline bool isEng() {
+    return getBuildType() == "eng";
+}
+inline bool isUser() {
+    return getBuildType() == "user";
+}
+inline bool isUserdebug() {
+    return getBuildType() == "userdebug";
+}
+
+}  // namespace bpf
+}  // namespace android
diff --git a/netd/BpfHandler.cpp b/netd/BpfHandler.cpp
index fc680d9..a7a4059 100644
--- a/netd/BpfHandler.cpp
+++ b/netd/BpfHandler.cpp
@@ -85,8 +85,24 @@
     // U bumps the kernel requirement up to 4.14
     if (modules::sdklevel::IsAtLeastU() && !bpf::isAtLeastKernelVersion(4, 14, 0)) abort();
 
-    // V bumps the kernel requirement up to 4.19
-    if (modules::sdklevel::IsAtLeastV() && !bpf::isAtLeastKernelVersion(4, 19, 0)) abort();
+    if (modules::sdklevel::IsAtLeastV()) {
+        // V bumps the kernel requirement up to 4.19
+        // see also: //system/netd/tests/kernel_test.cpp TestKernel419
+        if (!bpf::isAtLeastKernelVersion(4, 19, 0)) abort();
+
+        // Technically already required by U, but only enforce on V+
+        // see also: //system/netd/tests/kernel_test.cpp TestKernel64Bit
+        if (bpf::isKernel32Bit() && bpf::isAtLeastKernelVersion(5, 16, 0)) abort();
+    }
+
+    // Linux 6.1 is highest version supported by U, starting with V new kernels,
+    // ie. 6.2+ we are dropping various kernel/system userspace 32-on-64 hacks
+    // (for example "ANDROID: xfrm: remove in_compat_syscall() checks").
+    // Note: this check/enforcement only applies to *system* userspace code,
+    // it does not affect unprivileged apps, the 32-on-64 compatibility
+    // problems are AFAIK limited to various CAP_NET_ADMIN protected interfaces.
+    // see also: //system/bpf/bpfloader/BpfLoader.cpp main()
+    if (bpf::isUserspace32bit() && bpf::isAtLeastKernelVersion(6, 2, 0)) abort();
 
     // U mandates this mount point (though it should also be the case on T)
     if (modules::sdklevel::IsAtLeastU() && !!strcmp(cg2_path, "/sys/fs/cgroup")) abort();
@@ -113,6 +129,24 @@
         RETURN_IF_NOT_OK(
                 attachProgramToCgroup(CGROUP_SOCKET_PROG_PATH, cg_fd, BPF_CGROUP_INET_SOCK_CREATE));
     }
+
+    if (bpf::isAtLeastKernelVersion(4, 19, 0)) {
+        RETURN_IF_NOT_OK(attachProgramToCgroup(
+                "/sys/fs/bpf/netd_readonly/prog_block_bind4_block_port",
+                cg_fd, BPF_CGROUP_INET4_BIND));
+        RETURN_IF_NOT_OK(attachProgramToCgroup(
+                "/sys/fs/bpf/netd_readonly/prog_block_bind6_block_port",
+                cg_fd, BPF_CGROUP_INET6_BIND));
+
+        // This should trivially pass, since we just attached up above,
+        // but BPF_PROG_QUERY is only implemented on 4.19+ kernels.
+        if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_EGRESS) <= 0) abort();
+        if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_INGRESS) <= 0) abort();
+        if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_SOCK_CREATE) <= 0) abort();
+        if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET4_BIND) <= 0) abort();
+        if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET6_BIND) <= 0) abort();
+    }
+
     return netdutils::status::ok;
 }
 
@@ -214,7 +248,7 @@
     // which might toggle the live stats map and clean it.
     const auto countUidStatsEntries = [chargeUid, &totalEntryCount, &perUidEntryCount](
                                               const StatsKey& key,
-                                              const BpfMap<StatsKey, StatsValue>&) {
+                                              const BpfMapRO<StatsKey, StatsValue>&) {
         if (key.uid == chargeUid) {
             perUidEntryCount++;
         }
@@ -232,9 +266,8 @@
         return -EINVAL;
     }
 
-    BpfMap<StatsKey, StatsValue>& currentMap =
+    BpfMapRO<StatsKey, StatsValue>& currentMap =
             (configuration.value() == SELECT_MAP_A) ? mStatsMapA : mStatsMapB;
-    // 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: %s",
diff --git a/netd/BpfHandler.h b/netd/BpfHandler.h
index a6da4eb..9e69efc 100644
--- a/netd/BpfHandler.h
+++ b/netd/BpfHandler.h
@@ -59,10 +59,10 @@
     bool hasUpdateDeviceStatsPermission(uid_t uid);
 
     BpfMap<uint64_t, UidTagValue> mCookieTagMap;
-    BpfMap<StatsKey, StatsValue> mStatsMapA;
+    BpfMapRO<StatsKey, StatsValue> mStatsMapA;
     BpfMapRO<StatsKey, StatsValue> mStatsMapB;
     BpfMapRO<uint32_t, uint32_t> mConfigurationMap;
-    BpfMap<uint32_t, uint8_t> mUidPermissionMap;
+    BpfMapRO<uint32_t, uint8_t> mUidPermissionMap;
 
     // The limit on the number of stats entries a uid can have in the per uid stats map. BpfHandler
     // will block that specific uid from tagging new sockets after the limit is reached.
diff --git a/netd/BpfHandlerTest.cpp b/netd/BpfHandlerTest.cpp
index f5c9a68..b38fa16 100644
--- a/netd/BpfHandlerTest.cpp
+++ b/netd/BpfHandlerTest.cpp
@@ -49,7 +49,7 @@
     BpfHandler mBh;
     BpfMap<uint64_t, UidTagValue> mFakeCookieTagMap;
     BpfMap<StatsKey, StatsValue> mFakeStatsMapA;
-    BpfMapRO<uint32_t, uint32_t> mFakeConfigurationMap;
+    BpfMap<uint32_t, uint32_t> mFakeConfigurationMap;
     BpfMap<uint32_t, uint8_t> mFakeUidPermissionMap;
 
     void SetUp() {
diff --git a/remoteauth/service/Android.bp b/remoteauth/service/Android.bp
index 6e7b8d2..98ed2b2 100644
--- a/remoteauth/service/Android.bp
+++ b/remoteauth/service/Android.bp
@@ -18,7 +18,7 @@
 
 filegroup {
     name: "remoteauth-service-srcs",
-    srcs: ["java/**/*.java"],
+    srcs: [],
 }
 
 // Main lib for remoteauth services.
@@ -40,14 +40,10 @@
         "framework-statsd",
     ],
     static_libs: [
-        "guava",
-        "libprotobuf-java-lite",
-        "fast-pair-lite-protos",
         "modules-utils-build",
         "modules-utils-handlerexecutor",
         "modules-utils-preconditions",
         "modules-utils-backgroundthread",
-        "presence-lite-protos",
         "uwb_androidx_backend",
     ],
     sdk_version: "system_server_current",
diff --git a/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingParameters.java b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingParameters.java
index 923730c..4b5874b 100644
--- a/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingParameters.java
+++ b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingParameters.java
@@ -15,5 +15,67 @@
  */
 package com.android.server.remoteauth.ranging;
 
-/** The set of parameters to start ranging. */
-public class RangingParameters {}
+import androidx.core.uwb.backend.impl.internal.UwbAddress;
+
+/** The set of parameters to initiate {@link RangingSession#start}. */
+public class RangingParameters {
+
+    /** Parameters for {@link UwbRangingSession}. */
+    private final UwbAddress mUwbLocalAddress;
+
+    private final androidx.core.uwb.backend.impl.internal.RangingParameters mUwbRangingParameters;
+
+    public UwbAddress getUwbLocalAddress() {
+        return mUwbLocalAddress;
+    }
+
+    public androidx.core.uwb.backend.impl.internal.RangingParameters getUwbRangingParameters() {
+        return mUwbRangingParameters;
+    }
+
+    private RangingParameters(
+            UwbAddress uwbLocalAddress,
+            androidx.core.uwb.backend.impl.internal.RangingParameters uwbRangingParameters) {
+        mUwbLocalAddress = uwbLocalAddress;
+        mUwbRangingParameters = uwbRangingParameters;
+    }
+
+    /** Builder class for {@link RangingParameters}. */
+    public static final class Builder {
+        private UwbAddress mUwbLocalAddress;
+        private androidx.core.uwb.backend.impl.internal.RangingParameters mUwbRangingParameters;
+
+        /**
+         * Sets the uwb local address.
+         *
+         * <p>Only required if {@link SessionParameters#getRangingMethod}=={@link
+         * RANGING_METHOD_UWB} and {@link SessionParameters#getAutoDeriveParams} == false
+         */
+        public Builder setUwbLocalAddress(UwbAddress uwbLocalAddress) {
+            mUwbLocalAddress = uwbLocalAddress;
+            return this;
+        }
+
+        /**
+         * Sets the uwb ranging parameters.
+         *
+         * <p>Only required if {@link SessionParameters#getRangingMethod}=={@link
+         * RANGING_METHOD_UWB}.
+         *
+         * <p>If {@link SessionParameters#getAutoDeriveParams} == true, all required uwb parameters
+         * including uwbLocalAddress, complexChannel, peerAddresses, and sessionKeyInfo will be
+         * automatically derived, so unnecessary to provide and the other uwb parameters are
+         * optional.
+         */
+        public Builder setUwbRangingParameters(
+                androidx.core.uwb.backend.impl.internal.RangingParameters uwbRangingParameters) {
+            mUwbRangingParameters = uwbRangingParameters;
+            return this;
+        }
+
+        /** Builds {@link RangingParameters}. */
+        public RangingParameters build() {
+            return new RangingParameters(mUwbLocalAddress, mUwbRangingParameters);
+        }
+    }
+}
diff --git a/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingSession.java b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingSession.java
index adb36c5..a922168 100644
--- a/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingSession.java
+++ b/remoteauth/service/java/com/android/server/remoteauth/ranging/RangingSession.java
@@ -37,7 +37,8 @@
  * <p>A session can be started and stopped multiple times. After starting, updates ({@link
  * RangingReport}, {@link RangingError}, etc) will be reported via the provided {@link
  * RangingCallback}. BaseKey and SyncData are used for auto derivation of supported ranging
- * parameters, which will be implementation specific.
+ * parameters, which will be implementation specific. All session creation shall only be conducted
+ * via {@link RangingManager#createSession}.
  *
  * <p>Ranging method specific implementation shall be implemented in the extended class.
  */
diff --git a/remoteauth/service/java/com/android/server/remoteauth/ranging/UwbRangingSession.java b/remoteauth/service/java/com/android/server/remoteauth/ranging/UwbRangingSession.java
index 2015b66..62463e1 100644
--- a/remoteauth/service/java/com/android/server/remoteauth/ranging/UwbRangingSession.java
+++ b/remoteauth/service/java/com/android/server/remoteauth/ranging/UwbRangingSession.java
@@ -15,30 +15,219 @@
  */
 package com.android.server.remoteauth.ranging;
 
-import android.annotation.NonNull;
-import android.content.Context;
+import static androidx.core.uwb.backend.impl.internal.RangingDevice.SESSION_ID_UNSET;
+import static androidx.core.uwb.backend.impl.internal.Utils.STATUS_OK;
+import static androidx.core.uwb.backend.impl.internal.Utils.SUPPORTED_BPRF_PREAMBLE_INDEX;
+import static androidx.core.uwb.backend.impl.internal.UwbAddress.SHORT_ADDRESS_LENGTH;
 
+import static com.android.server.remoteauth.ranging.RangingReport.PROXIMITY_STATE_INSIDE;
+import static com.android.server.remoteauth.ranging.RangingReport.PROXIMITY_STATE_OUTSIDE;
+import static com.android.server.remoteauth.ranging.SessionParameters.DEVICE_ROLE_INITIATOR;
+
+import static com.google.uwb.support.fira.FiraParams.UWB_CHANNEL_9;
+
+import android.annotation.NonNull;
+import android.annotation.RequiresApi;
+import android.content.Context;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.core.uwb.backend.impl.internal.RangingController;
+import androidx.core.uwb.backend.impl.internal.RangingDevice;
+import androidx.core.uwb.backend.impl.internal.RangingPosition;
+import androidx.core.uwb.backend.impl.internal.RangingSessionCallback;
+import androidx.core.uwb.backend.impl.internal.RangingSessionCallback.RangingSuspendedReason;
+import androidx.core.uwb.backend.impl.internal.UwbAddress;
+import androidx.core.uwb.backend.impl.internal.UwbComplexChannel;
+import androidx.core.uwb.backend.impl.internal.UwbDevice;
 import androidx.core.uwb.backend.impl.internal.UwbServiceImpl;
 
-import java.util.concurrent.Executor;
+import com.android.internal.util.Preconditions;
 
-/** UWB (ultra wide-band) implementation of {@link RangingSession}. */
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+/** UWB (ultra wide-band) specific implementation of {@link RangingSession}. */
+@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
 public class UwbRangingSession extends RangingSession {
-    private static final int DERIVED_DATA_LENGTH = 1;
+    private static final String TAG = "UwbRangingSession";
+
+    private static final int COMPLEX_CHANNEL_LENGTH = 1;
+    private static final int STS_KEY_LENGTH = 16;
+    private static final int DERIVED_DATA_LENGTH =
+            COMPLEX_CHANNEL_LENGTH + SHORT_ADDRESS_LENGTH + SHORT_ADDRESS_LENGTH + STS_KEY_LENGTH;
+
+    private final UwbServiceImpl mUwbServiceImpl;
+    private final RangingDevice mRangingDevice;
+
+    private Executor mExecutor;
+    private RangingCallback mRangingCallback;
 
     public UwbRangingSession(
             @NonNull Context context,
             @NonNull SessionParameters sessionParameters,
             @NonNull UwbServiceImpl uwbServiceImpl) {
         super(context, sessionParameters, DERIVED_DATA_LENGTH);
+        Preconditions.checkNotNull(uwbServiceImpl);
+        mUwbServiceImpl = uwbServiceImpl;
+        if (sessionParameters.getDeviceRole() == DEVICE_ROLE_INITIATOR) {
+            mRangingDevice = (RangingDevice) mUwbServiceImpl.getController(context);
+        } else {
+            mRangingDevice = (RangingDevice) mUwbServiceImpl.getControlee(context);
+        }
     }
 
     @Override
     public void start(
             @NonNull RangingParameters rangingParameters,
             @NonNull Executor executor,
-            @NonNull RangingCallback rangingCallback) {}
+            @NonNull RangingCallback rangingCallback) {
+        Preconditions.checkNotNull(rangingParameters, "rangingParameters must not be null");
+        Preconditions.checkNotNull(executor, "executor must not be null");
+        Preconditions.checkNotNull(rangingCallback, "rangingCallback must not be null");
+
+        setUwbRangingParameters(rangingParameters);
+        int status =
+                mRangingDevice.startRanging(
+                        convertCallback(rangingCallback, executor),
+                        Executors.newSingleThreadExecutor());
+        if (status != STATUS_OK) {
+            Log.w(TAG, String.format("Uwb ranging start failed with status %d", status));
+            executor.execute(
+                    () -> rangingCallback.onError(mSessionInfo, RANGING_ERROR_FAILED_TO_START));
+            return;
+        }
+        mExecutor = executor;
+        mRangingCallback = rangingCallback;
+        Log.i(TAG, "start");
+    }
 
     @Override
-    public void stop() {}
+    public void stop() {
+        if (mRangingCallback == null) {
+            Log.w(TAG, String.format("Failed to stop unstarted session"));
+            return;
+        }
+        int status = mRangingDevice.stopRanging();
+        if (status != STATUS_OK) {
+            Log.w(TAG, String.format("Uwb ranging stop failed with status %d", status));
+            mExecutor.execute(
+                    () -> mRangingCallback.onError(mSessionInfo, RANGING_ERROR_FAILED_TO_STOP));
+            return;
+        }
+        mRangingCallback = null;
+        Log.i(TAG, "stop");
+    }
+
+    private void setUwbRangingParameters(RangingParameters rangingParameters) {
+        androidx.core.uwb.backend.impl.internal.RangingParameters params =
+                rangingParameters.getUwbRangingParameters();
+        Preconditions.checkNotNull(params, "uwbRangingParameters must not be null");
+        if (mAutoDeriveParams) {
+            Preconditions.checkArgument(mDerivedData.length == DERIVED_DATA_LENGTH);
+            ByteBuffer buffer = ByteBuffer.wrap(mDerivedData);
+
+            byte complexChannelByte = buffer.get();
+            int preambleIndex =
+                    SUPPORTED_BPRF_PREAMBLE_INDEX.get(
+                            Math.abs(complexChannelByte) % SUPPORTED_BPRF_PREAMBLE_INDEX.size());
+            // Selecting channel 9 since it's the only mandatory channel.
+            UwbComplexChannel complexChannel = new UwbComplexChannel(UWB_CHANNEL_9, preambleIndex);
+
+            byte[] localAddress = new byte[SHORT_ADDRESS_LENGTH];
+            byte[] peerAddress = new byte[SHORT_ADDRESS_LENGTH];
+            if (mRangingDevice instanceof RangingController) {
+                ((RangingController) mRangingDevice).setComplexChannel(complexChannel);
+                buffer.get(localAddress);
+                buffer.get(peerAddress);
+            } else {
+                buffer.get(peerAddress);
+                buffer.get(localAddress);
+            }
+            byte[] stsKey = new byte[STS_KEY_LENGTH];
+            buffer.get(stsKey);
+
+            mRangingDevice.setLocalAddress(UwbAddress.fromBytes(localAddress));
+            mRangingDevice.setRangingParameters(
+                    new androidx.core.uwb.backend.impl.internal.RangingParameters(
+                            params.getUwbConfigId(),
+                            SESSION_ID_UNSET,
+                            /* subSessionId= */ SESSION_ID_UNSET,
+                            stsKey,
+                            /* subSessionInfo= */ new byte[] {},
+                            complexChannel,
+                            List.of(UwbAddress.fromBytes(peerAddress)),
+                            params.getRangingUpdateRate(),
+                            params.getUwbRangeDataNtfConfig(),
+                            params.getSlotDuration(),
+                            params.isAoaDisabled()));
+        } else {
+            UwbAddress localAddress = rangingParameters.getUwbLocalAddress();
+            Preconditions.checkNotNull(localAddress, "localAddress must not be null");
+            UwbComplexChannel complexChannel = params.getComplexChannel();
+            Preconditions.checkNotNull(complexChannel, "complexChannel must not be null");
+            mRangingDevice.setLocalAddress(localAddress);
+            if (mRangingDevice instanceof RangingController) {
+                ((RangingController) mRangingDevice).setComplexChannel(complexChannel);
+            }
+            mRangingDevice.setRangingParameters(params);
+        }
+    }
+
+    private RangingSessionCallback convertCallback(RangingCallback callback, Executor executor) {
+        return new RangingSessionCallback() {
+
+            @Override
+            public void onRangingInitialized(UwbDevice device) {
+                Log.i(TAG, "onRangingInitialized");
+            }
+
+            @Override
+            public void onRangingResult(UwbDevice device, RangingPosition position) {
+                float distanceM = position.getDistance().getValue();
+                int proximityState =
+                        (mLowerProximityBoundaryM <= distanceM
+                                        && distanceM <= mUpperProximityBoundaryM)
+                                ? PROXIMITY_STATE_INSIDE
+                                : PROXIMITY_STATE_OUTSIDE;
+                position.getDistance().getValue();
+                RangingReport rangingReport =
+                        new RangingReport.Builder()
+                                .setDistanceM(distanceM)
+                                .setProximityState(proximityState)
+                                .build();
+                executor.execute(() -> callback.onRangingReport(mSessionInfo, rangingReport));
+            }
+
+            @Override
+            public void onRangingSuspended(UwbDevice device, @RangingSuspendedReason int reason) {
+                executor.execute(() -> callback.onError(mSessionInfo, convertError(reason)));
+            }
+        };
+    }
+
+    @RangingError
+    private static int convertError(@RangingSuspendedReason int reason) {
+        if (reason == RangingSessionCallback.REASON_WRONG_PARAMETERS) {
+            return RANGING_ERROR_INVALID_PARAMETERS;
+        }
+        if (reason == RangingSessionCallback.REASON_STOP_RANGING_CALLED) {
+            return RANGING_ERROR_STOPPED_BY_REQUEST;
+        }
+        if (reason == RangingSessionCallback.REASON_STOPPED_BY_PEER) {
+            return RANGING_ERROR_STOPPED_BY_PEER;
+        }
+        if (reason == RangingSessionCallback.REASON_FAILED_TO_START) {
+            return RANGING_ERROR_FAILED_TO_START;
+        }
+        if (reason == RangingSessionCallback.REASON_SYSTEM_POLICY) {
+            return RANGING_ERROR_SYSTEM_ERROR;
+        }
+        if (reason == RangingSessionCallback.REASON_MAX_RANGING_ROUND_RETRY_REACHED) {
+            return RANGING_ERROR_SYSTEM_TIMEOUT;
+        }
+        return RANGING_ERROR_UNKNOWN;
+    }
 }
diff --git a/remoteauth/tests/unit/Android.bp b/remoteauth/tests/unit/Android.bp
index 37c78c7..a21c033 100644
--- a/remoteauth/tests/unit/Android.bp
+++ b/remoteauth/tests/unit/Android.bp
@@ -20,13 +20,13 @@
     name: "RemoteAuthUnitTests",
     defaults: [
         "enable-remoteauth-targets",
-        "mts-target-sdk-version-current"
+        "mts-target-sdk-version-current",
     ],
     sdk_version: "test_current",
     min_sdk_version: "31",
 
     // Include all test java files.
-    srcs: ["src/**/*.java"],
+    srcs: [],
 
     libs: [
         "android.test.base",
@@ -45,7 +45,7 @@
         "mockito-target-extended-minus-junit4",
         "platform-test-annotations",
         "service-remoteauth-pre-jarjar",
-        "truth-prebuilt",
+        "truth",
     ],
     // these are needed for Extended Mockito
     jni_libs: [
diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingParametersTest.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingParametersTest.java
new file mode 100644
index 0000000..3be5e70
--- /dev/null
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/RangingParametersTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.ranging;
+
+import static androidx.core.uwb.backend.impl.internal.RangingDevice.SESSION_ID_UNSET;
+import static androidx.core.uwb.backend.impl.internal.Utils.CONFIG_PROVISIONED_UNICAST_DS_TWR;
+import static androidx.core.uwb.backend.impl.internal.Utils.DURATION_1_MS;
+import static androidx.core.uwb.backend.impl.internal.Utils.NORMAL;
+
+import static com.google.uwb.support.fira.FiraParams.UWB_CHANNEL_9;
+
+import static org.junit.Assert.assertEquals;
+
+import androidx.core.uwb.backend.impl.internal.UwbAddress;
+import androidx.core.uwb.backend.impl.internal.UwbComplexChannel;
+import androidx.core.uwb.backend.impl.internal.UwbRangeDataNtfConfig;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+/** Unit test for {@link RangingParameters}. */
+@RunWith(AndroidJUnit4.class)
+public class RangingParametersTest {
+
+    private static final UwbAddress TEST_UWB_LOCAL_ADDRESS =
+            UwbAddress.fromBytes(new byte[] {0x00, 0x01});
+    private static final androidx.core.uwb.backend.impl.internal.RangingParameters
+            TEST_UWB_RANGING_PARAMETERS =
+                    new androidx.core.uwb.backend.impl.internal.RangingParameters(
+                            CONFIG_PROVISIONED_UNICAST_DS_TWR,
+                            /* sessionId= */ SESSION_ID_UNSET,
+                            /* subSessionId= */ SESSION_ID_UNSET,
+                            /* SessionInfo= */ new byte[] {},
+                            /* subSessionInfo= */ new byte[] {},
+                            new UwbComplexChannel(UWB_CHANNEL_9, /* preambleIndex= */ 9),
+                            List.of(UwbAddress.fromBytes(new byte[] {0x00, 0x02})),
+                            /* rangingUpdateRate= */ NORMAL,
+                            new UwbRangeDataNtfConfig.Builder().build(),
+                            /* slotDuration= */ DURATION_1_MS,
+                            /* isAoaDisabled= */ false);
+
+    @Test
+    public void testBuildingRangingParameters_success() {
+        final RangingParameters rangingParameters =
+                new RangingParameters.Builder()
+                        .setUwbLocalAddress(TEST_UWB_LOCAL_ADDRESS)
+                        .setUwbRangingParameters(TEST_UWB_RANGING_PARAMETERS)
+                        .build();
+
+        assertEquals(rangingParameters.getUwbLocalAddress(), TEST_UWB_LOCAL_ADDRESS);
+        assertEquals(rangingParameters.getUwbRangingParameters(), TEST_UWB_RANGING_PARAMETERS);
+    }
+}
diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/UwbRangingSessionTest.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/UwbRangingSessionTest.java
new file mode 100644
index 0000000..91198ab
--- /dev/null
+++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/ranging/UwbRangingSessionTest.java
@@ -0,0 +1,375 @@
+/*
+ * 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.ranging;
+
+import static androidx.core.uwb.backend.impl.internal.RangingDevice.SESSION_ID_UNSET;
+import static androidx.core.uwb.backend.impl.internal.RangingMeasurement.CONFIDENCE_HIGH;
+import static androidx.core.uwb.backend.impl.internal.Utils.CONFIG_PROVISIONED_UNICAST_DS_TWR;
+import static androidx.core.uwb.backend.impl.internal.Utils.DURATION_1_MS;
+import static androidx.core.uwb.backend.impl.internal.Utils.NORMAL;
+import static androidx.core.uwb.backend.impl.internal.Utils.STATUS_ERROR;
+import static androidx.core.uwb.backend.impl.internal.Utils.STATUS_OK;
+
+import static com.android.server.remoteauth.ranging.RangingCapabilities.RANGING_METHOD_UWB;
+import static com.android.server.remoteauth.ranging.RangingReport.PROXIMITY_STATE_INSIDE;
+import static com.android.server.remoteauth.ranging.RangingSession.RANGING_ERROR_FAILED_TO_START;
+import static com.android.server.remoteauth.ranging.RangingSession.RANGING_ERROR_FAILED_TO_STOP;
+import static com.android.server.remoteauth.ranging.SessionParameters.DEVICE_ROLE_INITIATOR;
+import static com.android.server.remoteauth.ranging.SessionParameters.DEVICE_ROLE_RESPONDER;
+
+import static com.google.uwb.support.fira.FiraParams.UWB_CHANNEL_9;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+
+import androidx.core.uwb.backend.impl.internal.RangingControlee;
+import androidx.core.uwb.backend.impl.internal.RangingController;
+import androidx.core.uwb.backend.impl.internal.RangingMeasurement;
+import androidx.core.uwb.backend.impl.internal.RangingPosition;
+import androidx.core.uwb.backend.impl.internal.RangingSessionCallback;
+import androidx.core.uwb.backend.impl.internal.UwbAddress;
+import androidx.core.uwb.backend.impl.internal.UwbComplexChannel;
+import androidx.core.uwb.backend.impl.internal.UwbDevice;
+import androidx.core.uwb.backend.impl.internal.UwbRangeDataNtfConfig;
+import androidx.core.uwb.backend.impl.internal.UwbServiceImpl;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.remoteauth.ranging.RangingCapabilities.RangingMethod;
+import com.android.server.remoteauth.ranging.RangingSession.RangingCallback;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/** Unit test for {@link UwbRangingSession}. */
+@RunWith(AndroidJUnit4.class)
+public class UwbRangingSessionTest {
+
+    private static final String TEST_DEVICE_ID = "test_device_id";
+    @RangingMethod private static final int TEST_RANGING_METHOD = RANGING_METHOD_UWB;
+    private static final float TEST_LOWER_PROXIMITY_BOUNDARY_M = 1.0f;
+    private static final float TEST_UPPER_PROXIMITY_BOUNDARY_M = 2.5f;
+    private static final boolean TEST_AUTO_DERIVE_PARAMS = true;
+    private static final byte[] TEST_BASE_KEY =
+            new byte[] {
+                0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
+                0x0e, 0x0f
+            };
+    private static final byte[] TEST_SYNC_DATA =
+            new byte[] {
+                0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
+                0x0f, 0x00
+            };
+    private static final SessionParameters TEST_SESSION_PARAMETER_INITIATOR =
+            new SessionParameters.Builder()
+                    .setDeviceId(TEST_DEVICE_ID)
+                    .setRangingMethod(TEST_RANGING_METHOD)
+                    .setDeviceRole(DEVICE_ROLE_INITIATOR)
+                    .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                    .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                    .build();
+    private static final SessionParameters TEST_SESSION_PARAMETER_RESPONDER =
+            new SessionParameters.Builder()
+                    .setDeviceId(TEST_DEVICE_ID)
+                    .setRangingMethod(TEST_RANGING_METHOD)
+                    .setDeviceRole(DEVICE_ROLE_RESPONDER)
+                    .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                    .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                    .build();
+    private static final SessionParameters TEST_SESSION_PARAMETER_INITIATOR_W_AD =
+            new SessionParameters.Builder()
+                    .setDeviceId(TEST_DEVICE_ID)
+                    .setRangingMethod(TEST_RANGING_METHOD)
+                    .setDeviceRole(DEVICE_ROLE_INITIATOR)
+                    .setLowerProximityBoundaryM(TEST_LOWER_PROXIMITY_BOUNDARY_M)
+                    .setUpperProximityBoundaryM(TEST_UPPER_PROXIMITY_BOUNDARY_M)
+                    .setAutoDeriveParams(TEST_AUTO_DERIVE_PARAMS)
+                    .setBaseKey(TEST_BASE_KEY)
+                    .setSyncData(TEST_SYNC_DATA)
+                    .build();
+    private static final UwbAddress TEST_UWB_LOCAL_ADDRESS =
+            UwbAddress.fromBytes(new byte[] {0x00, 0x01});
+    private static final UwbAddress TEST_UWB_PEER_ADDRESS =
+            UwbAddress.fromBytes(new byte[] {0x00, 0x02});
+    private static final UwbComplexChannel TEST_UWB_COMPLEX_CHANNEL =
+            new UwbComplexChannel(UWB_CHANNEL_9, /* preambleIndex= */ 9);
+    private static final androidx.core.uwb.backend.impl.internal.RangingParameters
+            TEST_UWB_RANGING_PARAMETERS =
+                    new androidx.core.uwb.backend.impl.internal.RangingParameters(
+                            CONFIG_PROVISIONED_UNICAST_DS_TWR,
+                            /* sessionId= */ SESSION_ID_UNSET,
+                            /* subSessionId= */ SESSION_ID_UNSET,
+                            /* SessionInfo= */ new byte[] {},
+                            /* subSessionInfo= */ new byte[] {},
+                            TEST_UWB_COMPLEX_CHANNEL,
+                            List.of(TEST_UWB_PEER_ADDRESS),
+                            NORMAL,
+                            new UwbRangeDataNtfConfig.Builder().build(),
+                            DURATION_1_MS,
+                            /* isAoaDisabled= */ false);
+    private static final RangingParameters TEST_RANGING_PARAMETERS =
+            new RangingParameters.Builder()
+                    .setUwbLocalAddress(TEST_UWB_LOCAL_ADDRESS)
+                    .setUwbRangingParameters(TEST_UWB_RANGING_PARAMETERS)
+                    .build();
+    private static final UwbAddress TEST_DERIVED_UWB_LOCAL_ADDRESS =
+            UwbAddress.fromBytes(new byte[] {0x4C, (byte) 0xB4});
+    private static final UwbAddress TEST_DERIVED_UWB_PEER_ADDRESS =
+            UwbAddress.fromBytes(new byte[] {(byte) 0xAE, 0x2E});
+    private static final UwbComplexChannel TEST_DERIVED_UWB_COMPLEX_CHANNEL =
+            new UwbComplexChannel(UWB_CHANNEL_9, /* preambleIndex= */ 12);
+    private static final byte[] TEST_DERIVED_STS_KEY =
+            new byte[] {
+                0x76,
+                (byte) 0xD7,
+                (byte) 0xB6,
+                0x1A,
+                (byte) 0x8D,
+                0x29,
+                0x1A,
+                0x52,
+                (byte) 0xBB,
+                (byte) 0xBF,
+                (byte) 0xE6,
+                0x28,
+                (byte) 0xAD,
+                0x44,
+                (byte) 0xFB,
+                0x2E
+            };
+
+    private static final UwbDevice TEST_UWB_DEVICE =
+            UwbDevice.createForAddress(TEST_UWB_PEER_ADDRESS.toBytes());
+    private static final float TEST_DISTANCE = 1.5f;
+    private static final RangingMeasurement TEST_RANGING_MEASUREMENT =
+            new RangingMeasurement(
+                    /* confidence= */ CONFIDENCE_HIGH,
+                    /* value= */ TEST_DISTANCE,
+                    /* valid= */ true);
+    private static final RangingPosition TEST_RANGING_POSITION =
+            new RangingPosition(
+                    TEST_RANGING_MEASUREMENT,
+                    /* azimuth= */ null,
+                    /* elevation= */ null,
+                    /* dlTdoaMeasurement= */ null,
+                    /* elapsedRealtimeNanos= */ 0,
+                    /* rssi= */ 0);
+
+    @Mock private Context mContext;
+    @Mock private UwbServiceImpl mUwbServiceImpl;
+    @Mock private RangingController mRangingController;
+    @Mock private RangingControlee mRangingControlee;
+    @Mock private RangingCallback mRangingCallback;
+    @Mock private Executor mCallbackExecutor;
+
+    private UwbRangingSession mUwbRangingSession;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        when(mUwbServiceImpl.getController(mContext)).thenReturn(mRangingController);
+        when(mUwbServiceImpl.getControlee(mContext)).thenReturn(mRangingControlee);
+        when(mRangingController.startRanging(any(), any())).thenReturn(STATUS_OK);
+        when(mRangingControlee.startRanging(any(), any())).thenReturn(STATUS_OK);
+        doAnswer(
+                invocation -> {
+                    Runnable t = invocation.getArgument(0);
+                    t.run();
+                    return true;
+                })
+                .when(mCallbackExecutor)
+                .execute(any(Runnable.class));
+    }
+
+    @Test
+    public void testConstruction_nullArgument() {
+        assertThrows(
+                NullPointerException.class,
+                () ->
+                        new UwbRangingSession(
+                                null, TEST_SESSION_PARAMETER_INITIATOR, mUwbServiceImpl));
+        assertThrows(
+                NullPointerException.class,
+                () -> new UwbRangingSession(mContext, null, mUwbServiceImpl));
+        assertThrows(
+                NullPointerException.class,
+                () -> new UwbRangingSession(mContext, TEST_SESSION_PARAMETER_INITIATOR, null));
+    }
+
+    @Test
+    public void testConstruction_initiatorSuccess() {
+        mUwbRangingSession =
+                new UwbRangingSession(mContext, TEST_SESSION_PARAMETER_INITIATOR, mUwbServiceImpl);
+        verify(mUwbServiceImpl, times(1)).getController(mContext);
+    }
+
+    @Test
+    public void testConstruction_responderSuccess() {
+        mUwbRangingSession =
+                new UwbRangingSession(mContext, TEST_SESSION_PARAMETER_RESPONDER, mUwbServiceImpl);
+        verify(mUwbServiceImpl, times(1)).getControlee(mContext);
+    }
+
+    @Test
+    public void testStart_nullArgument() {
+        mUwbRangingSession =
+                new UwbRangingSession(mContext, TEST_SESSION_PARAMETER_INITIATOR, mUwbServiceImpl);
+
+        assertThrows(
+                NullPointerException.class,
+                () -> mUwbRangingSession.start(TEST_RANGING_PARAMETERS, mCallbackExecutor, null));
+        assertThrows(
+                NullPointerException.class,
+                () -> mUwbRangingSession.start(null, mCallbackExecutor, mRangingCallback));
+        assertThrows(
+                NullPointerException.class,
+                () -> mUwbRangingSession.start(TEST_RANGING_PARAMETERS, null, mRangingCallback));
+        assertThrows(
+                NullPointerException.class,
+                () ->
+                        mUwbRangingSession.start(
+                                new RangingParameters.Builder().build(),
+                                mCallbackExecutor,
+                                mRangingCallback));
+    }
+
+    @Test
+    public void testStart_initiatorWithoutADFailed() {
+        when(mRangingController.startRanging(any(), any())).thenReturn(STATUS_ERROR);
+
+        mUwbRangingSession =
+                new UwbRangingSession(mContext, TEST_SESSION_PARAMETER_INITIATOR, mUwbServiceImpl);
+        mUwbRangingSession.start(TEST_RANGING_PARAMETERS, mCallbackExecutor, mRangingCallback);
+
+        verify(mRangingController, times(1)).setComplexChannel(TEST_UWB_COMPLEX_CHANNEL);
+        verify(mRangingController, times(1)).setLocalAddress(TEST_UWB_LOCAL_ADDRESS);
+        verify(mRangingController, times(1)).setRangingParameters(TEST_UWB_RANGING_PARAMETERS);
+        verify(mRangingController, times(1)).startRanging(any(), any());
+        ArgumentCaptor<SessionInfo> captor = ArgumentCaptor.forClass(SessionInfo.class);
+        verify(mRangingCallback, times(1))
+                .onError(captor.capture(), eq(RANGING_ERROR_FAILED_TO_START));
+        assertEquals(captor.getValue().getDeviceId(), TEST_DEVICE_ID);
+    }
+
+    private void testRangingCallback() {
+        Answer startRangingResponse =
+                new Answer() {
+                    public Object answer(InvocationOnMock invocation) {
+                        Object[] args = invocation.getArguments();
+                        RangingSessionCallback cb = (RangingSessionCallback) args[0];
+                        cb.onRangingInitialized(TEST_UWB_DEVICE);
+                        cb.onRangingResult(TEST_UWB_DEVICE, TEST_RANGING_POSITION);
+                        return STATUS_OK;
+                    }
+                };
+        doAnswer(startRangingResponse)
+                .when(mRangingController)
+                .startRanging(any(RangingSessionCallback.class), any());
+    }
+
+    @Test
+    public void testStart_initiatorWithADSucceed() {
+        testRangingCallback();
+        mUwbRangingSession =
+                new UwbRangingSession(
+                        mContext, TEST_SESSION_PARAMETER_INITIATOR_W_AD, mUwbServiceImpl);
+        mUwbRangingSession.start(TEST_RANGING_PARAMETERS, mCallbackExecutor, mRangingCallback);
+
+        verify(mRangingController, times(1)).setComplexChannel(TEST_DERIVED_UWB_COMPLEX_CHANNEL);
+        verify(mRangingController, times(1)).setLocalAddress(TEST_DERIVED_UWB_LOCAL_ADDRESS);
+        ArgumentCaptor<androidx.core.uwb.backend.impl.internal.RangingParameters> captor =
+                ArgumentCaptor.forClass(
+                        androidx.core.uwb.backend.impl.internal.RangingParameters.class);
+        verify(mRangingController, times(1)).setRangingParameters(captor.capture());
+        assertEquals(
+                captor.getValue().getUwbConfigId(), TEST_UWB_RANGING_PARAMETERS.getUwbConfigId());
+        assertEquals(captor.getValue().getSessionId(), SESSION_ID_UNSET);
+        assertEquals(captor.getValue().getSubSessionId(), SESSION_ID_UNSET);
+        assertArrayEquals(captor.getValue().getSessionKeyInfo(), TEST_DERIVED_STS_KEY);
+        assertArrayEquals(captor.getValue().getSubSessionKeyInfo(), new byte[] {});
+        assertEquals(captor.getValue().getComplexChannel(), TEST_DERIVED_UWB_COMPLEX_CHANNEL);
+        assertEquals(captor.getValue().getPeerAddresses().get(0), TEST_DERIVED_UWB_PEER_ADDRESS);
+        assertEquals(
+                captor.getValue().getRangingUpdateRate(),
+                TEST_UWB_RANGING_PARAMETERS.getRangingUpdateRate());
+        assertEquals(
+                captor.getValue().getUwbRangeDataNtfConfig(),
+                TEST_UWB_RANGING_PARAMETERS.getUwbRangeDataNtfConfig());
+        assertEquals(
+                captor.getValue().getSlotDuration(), TEST_UWB_RANGING_PARAMETERS.getSlotDuration());
+        assertEquals(
+                captor.getValue().isAoaDisabled(), TEST_UWB_RANGING_PARAMETERS.isAoaDisabled());
+        verify(mRangingController, times(1)).startRanging(any(), any());
+        ArgumentCaptor<SessionInfo> captor2 = ArgumentCaptor.forClass(SessionInfo.class);
+        ArgumentCaptor<RangingReport> captor3 = ArgumentCaptor.forClass(RangingReport.class);
+        verify(mRangingCallback, times(1)).onRangingReport(captor2.capture(), captor3.capture());
+        assertEquals(captor2.getValue().getDeviceId(), TEST_DEVICE_ID);
+        RangingReport rangingReport = captor3.getValue();
+        assertEquals(rangingReport.getDistanceM(), TEST_DISTANCE, 0.0f);
+        assertEquals(rangingReport.getProximityState(), PROXIMITY_STATE_INSIDE);
+    }
+
+    @Test
+    public void testStop_sessionNotStarted() {
+        when(mRangingController.stopRanging()).thenReturn(STATUS_ERROR);
+
+        mUwbRangingSession =
+                new UwbRangingSession(mContext, TEST_SESSION_PARAMETER_INITIATOR, mUwbServiceImpl);
+        mUwbRangingSession.stop();
+
+        verifyZeroInteractions(mRangingController);
+        verifyZeroInteractions(mRangingCallback);
+    }
+
+    @Test
+    public void testStop_failed() {
+        when(mRangingController.stopRanging()).thenReturn(STATUS_ERROR);
+
+        mUwbRangingSession =
+                new UwbRangingSession(mContext, TEST_SESSION_PARAMETER_INITIATOR, mUwbServiceImpl);
+        mUwbRangingSession.start(TEST_RANGING_PARAMETERS, mCallbackExecutor, mRangingCallback);
+        mUwbRangingSession.stop();
+
+        verify(mRangingController, times(1)).setComplexChannel(any());
+        verify(mRangingController, times(1)).setLocalAddress(any());
+        verify(mRangingController, times(1)).setRangingParameters(any());
+        verify(mRangingController, times(1)).startRanging(any(), any());
+        verify(mRangingController, times(1)).stopRanging();
+        ArgumentCaptor<SessionInfo> captor = ArgumentCaptor.forClass(SessionInfo.class);
+        verify(mRangingCallback, times(1))
+                .onError(captor.capture(), eq(RANGING_ERROR_FAILED_TO_STOP));
+        assertEquals(captor.getValue().getDeviceId(), TEST_DEVICE_ID);
+    }
+}
diff --git a/service-t/Android.bp b/service-t/Android.bp
index 0f14956..7e588cd 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -91,6 +91,10 @@
 java_library {
     name: "service-connectivity-mdns-standalone-build-test",
     sdk_version: "core_platform",
+    min_sdk_version: "21",
+    lint: {
+        error_checks: ["NewApi"],
+    },
     srcs: [
         "src/com/android/server/connectivity/mdns/**/*.java",
         ":framework-connectivity-t-mdns-standalone-build-sources",
@@ -98,7 +102,12 @@
     ],
     exclude_srcs: [
         "src/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitor.java",
-        "src/com/android/server/connectivity/mdns/SocketNetLinkMonitorFactory.java"
+        "src/com/android/server/connectivity/mdns/SocketNetLinkMonitorFactory.java",
+        "src/com/android/server/connectivity/mdns/MdnsAdvertiser.java",
+        "src/com/android/server/connectivity/mdns/MdnsAnnouncer.java",
+        "src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java",
+        "src/com/android/server/connectivity/mdns/MdnsProber.java",
+        "src/com/android/server/connectivity/mdns/MdnsRecordRepository.java",
     ],
     static_libs: [
         "net-utils-device-common-mdns-standalone-build-test",
diff --git a/service-t/Sources.bp b/service-t/Sources.bp
index 187eadf..fbe02a5 100644
--- a/service-t/Sources.bp
+++ b/service-t/Sources.bp
@@ -20,7 +20,6 @@
     srcs: [
         "jni/com_android_server_net_NetworkStatsFactory.cpp",
     ],
-    path: "jni",
     visibility: [
         "//packages/modules/Connectivity:__subpackages__",
     ],
@@ -32,7 +31,6 @@
         "jni/com_android_server_net_NetworkStatsFactory.cpp",
         "jni/com_android_server_net_NetworkStatsService.cpp",
     ],
-    path: "jni",
     visibility: [
         "//packages/modules/Connectivity:__subpackages__",
     ],
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
index fed2979..3101397 100644
--- a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
@@ -41,7 +41,7 @@
 using base::Result;
 
 int bpfGetUidStatsInternal(uid_t uid, StatsValue* stats,
-                           const BpfMap<uint32_t, StatsValue>& appUidStatsMap) {
+                           const BpfMapRO<uint32_t, StatsValue>& appUidStatsMap) {
     auto statsEntry = appUidStatsMap.readValue(uid);
     if (!statsEntry.ok()) {
         *stats = {};
@@ -57,14 +57,14 @@
 }
 
 int bpfGetIfaceStatsInternal(const char* iface, StatsValue* stats,
-                             const BpfMap<uint32_t, StatsValue>& ifaceStatsMap,
-                             const BpfMap<uint32_t, IfaceValue>& ifaceNameMap) {
+                             const BpfMapRO<uint32_t, StatsValue>& ifaceStatsMap,
+                             const BpfMapRO<uint32_t, IfaceValue>& ifaceNameMap) {
     *stats = {};
     int64_t unknownIfaceBytesTotal = 0;
     const auto processIfaceStats =
             [iface, stats, &ifaceNameMap, &unknownIfaceBytesTotal](
                     const uint32_t& key,
-                    const BpfMap<uint32_t, StatsValue>& ifaceStatsMap) -> Result<void> {
+                    const BpfMapRO<uint32_t, StatsValue>& ifaceStatsMap) -> Result<void> {
         char ifname[IFNAMSIZ];
         if (getIfaceNameFromMap(ifaceNameMap, ifaceStatsMap, key, ifname, key,
                                 &unknownIfaceBytesTotal)) {
@@ -90,7 +90,7 @@
 }
 
 int bpfGetIfIndexStatsInternal(uint32_t ifindex, StatsValue* stats,
-                               const BpfMap<uint32_t, StatsValue>& ifaceStatsMap) {
+                               const BpfMapRO<uint32_t, StatsValue>& ifaceStatsMap) {
     auto statsEntry = ifaceStatsMap.readValue(ifindex);
     if (!statsEntry.ok()) {
         *stats = {};
@@ -120,13 +120,13 @@
 }
 
 int parseBpfNetworkStatsDetailInternal(std::vector<stats_line>& lines,
-                                       const BpfMap<StatsKey, StatsValue>& statsMap,
-                                       const BpfMap<uint32_t, IfaceValue>& ifaceMap) {
+                                       const BpfMapRO<StatsKey, StatsValue>& statsMap,
+                                       const BpfMapRO<uint32_t, IfaceValue>& ifaceMap) {
     int64_t unknownIfaceBytesTotal = 0;
     const auto processDetailUidStats =
             [&lines, &unknownIfaceBytesTotal, &ifaceMap](
                     const StatsKey& key,
-                    const BpfMap<StatsKey, StatsValue>& statsMap) -> Result<void> {
+                    const BpfMapRO<StatsKey, StatsValue>& statsMap) -> Result<void> {
         char ifname[IFNAMSIZ];
         if (getIfaceNameFromMap(ifaceMap, statsMap, key.ifaceIndex, ifname, key,
                                 &unknownIfaceBytesTotal)) {
@@ -212,12 +212,12 @@
 }
 
 int parseBpfNetworkStatsDevInternal(std::vector<stats_line>& lines,
-                                    const BpfMap<uint32_t, StatsValue>& statsMap,
-                                    const BpfMap<uint32_t, IfaceValue>& ifaceMap) {
+                                    const BpfMapRO<uint32_t, StatsValue>& statsMap,
+                                    const BpfMapRO<uint32_t, IfaceValue>& ifaceMap) {
     int64_t unknownIfaceBytesTotal = 0;
     const auto processDetailIfaceStats = [&lines, &unknownIfaceBytesTotal, &ifaceMap, &statsMap](
                                              const uint32_t& key, const StatsValue& value,
-                                             const BpfMap<uint32_t, StatsValue>&) {
+                                             const BpfMapRO<uint32_t, StatsValue>&) {
         char ifname[IFNAMSIZ];
         if (getIfaceNameFromMap(ifaceMap, statsMap, key, ifname, key, &unknownIfaceBytesTotal)) {
             return Result<void>();
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
index 76c56eb..bcc4550 100644
--- a/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
@@ -80,19 +80,19 @@
     void SetUp() {
         ASSERT_EQ(0, setrlimitForTest());
 
-        mFakeCookieTagMap = BpfMap<uint64_t, UidTagValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+        mFakeCookieTagMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
         ASSERT_TRUE(mFakeCookieTagMap.isValid());
 
-        mFakeAppUidStatsMap = BpfMap<uint32_t, StatsValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+        mFakeAppUidStatsMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
         ASSERT_TRUE(mFakeAppUidStatsMap.isValid());
 
-        mFakeStatsMap = BpfMap<StatsKey, StatsValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+        mFakeStatsMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
         ASSERT_TRUE(mFakeStatsMap.isValid());
 
-        mFakeIfaceIndexNameMap = BpfMap<uint32_t, IfaceValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+        mFakeIfaceIndexNameMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
         ASSERT_TRUE(mFakeIfaceIndexNameMap.isValid());
 
-        mFakeIfaceStatsMap = BpfMap<uint32_t, StatsValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+        mFakeIfaceStatsMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
         ASSERT_TRUE(mFakeIfaceStatsMap.isValid());
     }
 
diff --git a/service-t/native/libs/libnetworkstats/NetworkTraceHandler.cpp b/service-t/native/libs/libnetworkstats/NetworkTraceHandler.cpp
index ec63e41..9b1b72d 100644
--- a/service-t/native/libs/libnetworkstats/NetworkTraceHandler.cpp
+++ b/service-t/native/libs/libnetworkstats/NetworkTraceHandler.cpp
@@ -18,6 +18,7 @@
 
 #include "netdbpf/NetworkTraceHandler.h"
 
+#include <android-base/macros.h>
 #include <arpa/inet.h>
 #include <bpf/BpfUtils.h>
 #include <log/log.h>
@@ -75,9 +76,35 @@
   uint32_t bytes = 0;
 };
 
-#define AGG_FIELDS(x)                                              \
-  (x).ifindex, (x).uid, (x).tag, (x).sport, (x).dport, (x).egress, \
-      (x).ipProto, (x).tcpFlags
+BundleKey::BundleKey(const PacketTrace& pkt)
+    : ifindex(pkt.ifindex),
+      uid(pkt.uid),
+      tag(pkt.tag),
+      egress(pkt.egress),
+      ipProto(pkt.ipProto),
+      ipVersion(pkt.ipVersion) {
+  switch (ipProto) {
+    case IPPROTO_TCP:
+      tcpFlags = pkt.tcpFlags;
+      FALLTHROUGH_INTENDED;
+    case IPPROTO_DCCP:
+    case IPPROTO_UDP:
+    case IPPROTO_UDPLITE:
+    case IPPROTO_SCTP:
+      localPort = ntohs(pkt.egress ? pkt.sport : pkt.dport);
+      remotePort = ntohs(pkt.egress ? pkt.dport : pkt.sport);
+      break;
+    case IPPROTO_ICMP:
+    case IPPROTO_ICMPV6:
+      icmpType = ntohs(pkt.sport);
+      icmpCode = ntohs(pkt.dport);
+      break;
+  }
+}
+
+#define AGG_FIELDS(x)                                                    \
+  (x).ifindex, (x).uid, (x).tag, (x).egress, (x).ipProto, (x).ipVersion, \
+      (x).tcpFlags, (x).localPort, (x).remotePort, (x).icmpType, (x).icmpCode
 
 std::size_t BundleHash::operator()(const BundleKey& a) const {
   std::size_t seed = 0;
@@ -179,7 +206,7 @@
       dst->set_timestamp(pkt.timestampNs);
       auto* event = dst->set_network_packet();
       event->set_length(pkt.length);
-      Fill(pkt, event);
+      Fill(BundleKey(pkt), event);
     }
     return;
   }
@@ -187,14 +214,13 @@
   uint64_t minTs = std::numeric_limits<uint64_t>::max();
   std::unordered_map<BundleKey, BundleDetails, BundleHash, BundleEq> bundles;
   for (const PacketTrace& pkt : packets) {
-    BundleKey key = pkt;
+    BundleKey key(pkt);
 
     // Dropping fields should remove them from the output and remove them from
-    // the aggregation key. In order to do the latter without changing the hash
-    // function, set the dropped fields to zero.
-    if (mDropTcpFlags) key.tcpFlags = 0;
-    if (mDropLocalPort) (key.egress ? key.sport : key.dport) = 0;
-    if (mDropRemotePort) (key.egress ? key.dport : key.sport) = 0;
+    // the aggregation key. Reset the optionals to indicate omission.
+    if (mDropTcpFlags) key.tcpFlags.reset();
+    if (mDropLocalPort) key.localPort.reset();
+    if (mDropRemotePort) key.remotePort.reset();
 
     minTs = std::min(minTs, pkt.timestampNs);
 
@@ -245,22 +271,18 @@
   }
 }
 
-void NetworkTraceHandler::Fill(const PacketTrace& src,
+void NetworkTraceHandler::Fill(const BundleKey& src,
                                NetworkPacketEvent* event) {
   event->set_direction(src.egress ? TrafficDirection::DIR_EGRESS
                                   : TrafficDirection::DIR_INGRESS);
   event->set_uid(src.uid);
   event->set_tag(src.tag);
 
-  if (!mDropLocalPort) {
-    event->set_local_port(ntohs(src.egress ? src.sport : src.dport));
-  }
-  if (!mDropRemotePort) {
-    event->set_remote_port(ntohs(src.egress ? src.dport : src.sport));
-  }
-  if (!mDropTcpFlags) {
-    event->set_tcp_flags(src.tcpFlags);
-  }
+  if (src.tcpFlags.has_value()) event->set_tcp_flags(*src.tcpFlags);
+  if (src.localPort.has_value()) event->set_local_port(*src.localPort);
+  if (src.remotePort.has_value()) event->set_remote_port(*src.remotePort);
+  if (src.icmpType.has_value()) event->set_icmp_type(*src.icmpType);
+  if (src.icmpCode.has_value()) event->set_icmp_code(*src.icmpCode);
 
   event->set_ip_proto(src.ipProto);
 
diff --git a/service-t/native/libs/libnetworkstats/NetworkTraceHandlerTest.cpp b/service-t/native/libs/libnetworkstats/NetworkTraceHandlerTest.cpp
index f2c1a86..0c4f049 100644
--- a/service-t/native/libs/libnetworkstats/NetworkTraceHandlerTest.cpp
+++ b/service-t/native/libs/libnetworkstats/NetworkTraceHandlerTest.cpp
@@ -113,7 +113,7 @@
           .length = 100,
           .uid = 10,
           .tag = 123,
-          .ipProto = 6,
+          .ipProto = IPPROTO_TCP,
           .tcpFlags = 1,
       },
   };
@@ -138,12 +138,14 @@
           .sport = htons(8080),
           .dport = htons(443),
           .egress = true,
+          .ipProto = IPPROTO_TCP,
       },
       PacketTrace{
           .timestampNs = 2,
           .sport = htons(443),
           .dport = htons(8080),
           .egress = false,
+          .ipProto = IPPROTO_TCP,
       },
   };
 
@@ -161,6 +163,42 @@
               TrafficDirection::DIR_INGRESS);
 }
 
+TEST_F(NetworkTraceHandlerTest, WriteIcmpTypeAndCode) {
+  std::vector<PacketTrace> input = {
+      PacketTrace{
+          .timestampNs = 1,
+          .sport = htons(11),  // type
+          .dport = htons(22),  // code
+          .egress = true,
+          .ipProto = IPPROTO_ICMP,
+      },
+      PacketTrace{
+          .timestampNs = 2,
+          .sport = htons(33),  // type
+          .dport = htons(44),  // code
+          .egress = false,
+          .ipProto = IPPROTO_ICMPV6,
+      },
+  };
+
+  std::vector<TracePacket> events;
+  ASSERT_TRUE(TraceAndSortPackets(input, &events));
+
+  ASSERT_EQ(events.size(), 2);
+  EXPECT_FALSE(events[0].network_packet().has_local_port());
+  EXPECT_FALSE(events[0].network_packet().has_remote_port());
+  EXPECT_THAT(events[0].network_packet().icmp_type(), 11);
+  EXPECT_THAT(events[0].network_packet().icmp_code(), 22);
+  EXPECT_THAT(events[0].network_packet().direction(),
+              TrafficDirection::DIR_EGRESS);
+  EXPECT_FALSE(events[1].network_packet().local_port());
+  EXPECT_FALSE(events[1].network_packet().remote_port());
+  EXPECT_THAT(events[1].network_packet().icmp_type(), 33);
+  EXPECT_THAT(events[1].network_packet().icmp_code(), 44);
+  EXPECT_THAT(events[1].network_packet().direction(),
+              TrafficDirection::DIR_INGRESS);
+}
+
 TEST_F(NetworkTraceHandlerTest, BasicBundling) {
   // TODO: remove this once bundling becomes default. Until then, set arbitrary
   // aggregation threshold to enable bundling.
@@ -168,12 +206,12 @@
   config.set_aggregation_threshold(10);
 
   std::vector<PacketTrace> input = {
-      PacketTrace{.uid = 123, .timestampNs = 2, .length = 200},
-      PacketTrace{.uid = 123, .timestampNs = 1, .length = 100},
-      PacketTrace{.uid = 123, .timestampNs = 4, .length = 300},
+      PacketTrace{.timestampNs = 2, .length = 200, .uid = 123},
+      PacketTrace{.timestampNs = 1, .length = 100, .uid = 123},
+      PacketTrace{.timestampNs = 4, .length = 300, .uid = 123},
 
-      PacketTrace{.uid = 456, .timestampNs = 2, .length = 400},
-      PacketTrace{.uid = 456, .timestampNs = 4, .length = 100},
+      PacketTrace{.timestampNs = 2, .length = 400, .uid = 456},
+      PacketTrace{.timestampNs = 4, .length = 100, .uid = 456},
   };
 
   std::vector<TracePacket> events;
@@ -203,12 +241,12 @@
   config.set_aggregation_threshold(3);
 
   std::vector<PacketTrace> input = {
-      PacketTrace{.uid = 123, .timestampNs = 2, .length = 200},
-      PacketTrace{.uid = 123, .timestampNs = 1, .length = 100},
-      PacketTrace{.uid = 123, .timestampNs = 4, .length = 300},
+      PacketTrace{.timestampNs = 2, .length = 200, .uid = 123},
+      PacketTrace{.timestampNs = 1, .length = 100, .uid = 123},
+      PacketTrace{.timestampNs = 4, .length = 300, .uid = 123},
 
-      PacketTrace{.uid = 456, .timestampNs = 2, .length = 400},
-      PacketTrace{.uid = 456, .timestampNs = 4, .length = 100},
+      PacketTrace{.timestampNs = 2, .length = 400, .uid = 456},
+      PacketTrace{.timestampNs = 4, .length = 100, .uid = 456},
   };
 
   std::vector<TracePacket> events;
@@ -239,12 +277,17 @@
   __be16 b = htons(10001);
   std::vector<PacketTrace> input = {
       // Recall that local is `src` for egress and `dst` for ingress.
-      PacketTrace{.timestampNs = 1, .length = 2, .egress = true, .sport = a},
-      PacketTrace{.timestampNs = 2, .length = 4, .egress = false, .dport = a},
-      PacketTrace{.timestampNs = 3, .length = 6, .egress = true, .sport = b},
-      PacketTrace{.timestampNs = 4, .length = 8, .egress = false, .dport = b},
+      PacketTrace{.timestampNs = 1, .length = 2, .sport = a, .egress = true},
+      PacketTrace{.timestampNs = 2, .length = 4, .dport = a, .egress = false},
+      PacketTrace{.timestampNs = 3, .length = 6, .sport = b, .egress = true},
+      PacketTrace{.timestampNs = 4, .length = 8, .dport = b, .egress = false},
   };
 
+  // Set common fields.
+  for (PacketTrace& pkt : input) {
+    pkt.ipProto = IPPROTO_TCP;
+  }
+
   std::vector<TracePacket> events;
   ASSERT_TRUE(TraceAndSortPackets(input, &events, config));
   ASSERT_EQ(events.size(), 2);
@@ -274,12 +317,17 @@
   __be16 b = htons(80);
   std::vector<PacketTrace> input = {
       // Recall that remote is `dst` for egress and `src` for ingress.
-      PacketTrace{.timestampNs = 1, .length = 2, .egress = true, .dport = a},
-      PacketTrace{.timestampNs = 2, .length = 4, .egress = false, .sport = a},
-      PacketTrace{.timestampNs = 3, .length = 6, .egress = true, .dport = b},
-      PacketTrace{.timestampNs = 4, .length = 8, .egress = false, .sport = b},
+      PacketTrace{.timestampNs = 1, .length = 2, .dport = a, .egress = true},
+      PacketTrace{.timestampNs = 2, .length = 4, .sport = a, .egress = false},
+      PacketTrace{.timestampNs = 3, .length = 6, .dport = b, .egress = true},
+      PacketTrace{.timestampNs = 4, .length = 8, .sport = b, .egress = false},
   };
 
+  // Set common fields.
+  for (PacketTrace& pkt : input) {
+    pkt.ipProto = IPPROTO_TCP;
+  }
+
   std::vector<TracePacket> events;
   ASSERT_TRUE(TraceAndSortPackets(input, &events, config));
   ASSERT_EQ(events.size(), 2);
@@ -306,12 +354,17 @@
   config.set_aggregation_threshold(10);
 
   std::vector<PacketTrace> input = {
-      PacketTrace{.timestampNs = 1, .uid = 123, .length = 1, .tcpFlags = 1},
-      PacketTrace{.timestampNs = 2, .uid = 123, .length = 2, .tcpFlags = 2},
-      PacketTrace{.timestampNs = 3, .uid = 456, .length = 3, .tcpFlags = 1},
-      PacketTrace{.timestampNs = 4, .uid = 456, .length = 4, .tcpFlags = 2},
+      PacketTrace{.timestampNs = 1, .length = 1, .uid = 123, .tcpFlags = 1},
+      PacketTrace{.timestampNs = 2, .length = 2, .uid = 123, .tcpFlags = 2},
+      PacketTrace{.timestampNs = 3, .length = 3, .uid = 456, .tcpFlags = 1},
+      PacketTrace{.timestampNs = 4, .length = 4, .uid = 456, .tcpFlags = 2},
   };
 
+  // Set common fields.
+  for (PacketTrace& pkt : input) {
+    pkt.ipProto = IPPROTO_TCP;
+  }
+
   std::vector<TracePacket> events;
   ASSERT_TRUE(TraceAndSortPackets(input, &events, config));
 
diff --git a/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h b/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h
index ea068fc..8058d05 100644
--- a/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h
+++ b/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h
@@ -57,24 +57,25 @@
 
 // For test only
 int bpfGetUidStatsInternal(uid_t uid, StatsValue* stats,
-                           const BpfMap<uint32_t, StatsValue>& appUidStatsMap);
+                           const BpfMapRO<uint32_t, StatsValue>& appUidStatsMap);
 // For test only
 int bpfGetIfaceStatsInternal(const char* iface, StatsValue* stats,
-                             const BpfMap<uint32_t, StatsValue>& ifaceStatsMap,
-                             const BpfMap<uint32_t, IfaceValue>& ifaceNameMap);
+                             const BpfMapRO<uint32_t, StatsValue>& ifaceStatsMap,
+                             const BpfMapRO<uint32_t, IfaceValue>& ifaceNameMap);
 // For test only
 int bpfGetIfIndexStatsInternal(uint32_t ifindex, StatsValue* stats,
-                               const BpfMap<uint32_t, StatsValue>& ifaceStatsMap);
+                               const BpfMapRO<uint32_t, StatsValue>& ifaceStatsMap);
 // For test only
 int parseBpfNetworkStatsDetailInternal(std::vector<stats_line>& lines,
-                                       const BpfMap<StatsKey, StatsValue>& statsMap,
-                                       const BpfMap<uint32_t, IfaceValue>& ifaceMap);
+                                       const BpfMapRO<StatsKey, StatsValue>& statsMap,
+                                       const BpfMapRO<uint32_t, IfaceValue>& ifaceMap);
 // For test only
 int cleanStatsMapInternal(const base::unique_fd& cookieTagMap, const base::unique_fd& tagStatsMap);
 // For test only
 template <class Key>
-int getIfaceNameFromMap(const BpfMap<uint32_t, IfaceValue>& ifaceMap,
-                        const BpfMap<Key, StatsValue>& statsMap, uint32_t ifaceIndex, char* ifname,
+int getIfaceNameFromMap(const BpfMapRO<uint32_t, IfaceValue>& ifaceMap,
+                        const BpfMapRO<Key, StatsValue>& statsMap,
+                        uint32_t ifaceIndex, char* ifname,
                         const Key& curKey, int64_t* unknownIfaceBytesTotal) {
     auto iface = ifaceMap.readValue(ifaceIndex);
     if (!iface.ok()) {
@@ -86,7 +87,7 @@
 }
 
 template <class Key>
-void maybeLogUnknownIface(int ifaceIndex, const BpfMap<Key, StatsValue>& statsMap,
+void maybeLogUnknownIface(int ifaceIndex, const BpfMapRO<Key, StatsValue>& statsMap,
                           const Key& curKey, int64_t* unknownIfaceBytesTotal) {
     // Have we already logged an error?
     if (*unknownIfaceBytesTotal == -1) {
@@ -110,8 +111,8 @@
 
 // For test only
 int parseBpfNetworkStatsDevInternal(std::vector<stats_line>& lines,
-                                    const BpfMap<uint32_t, StatsValue>& statsMap,
-                                    const BpfMap<uint32_t, IfaceValue>& ifaceMap);
+                                    const BpfMapRO<uint32_t, StatsValue>& statsMap,
+                                    const BpfMapRO<uint32_t, IfaceValue>& ifaceMap);
 
 int bpfGetUidStats(uid_t uid, StatsValue* stats);
 int bpfGetIfaceStats(const char* iface, StatsValue* stats);
diff --git a/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTraceHandler.h b/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTraceHandler.h
index bc10e68..6bf186a 100644
--- a/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTraceHandler.h
+++ b/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTraceHandler.h
@@ -30,15 +30,33 @@
 namespace android {
 namespace bpf {
 
-// BundleKeys are PacketTraces where timestamp and length are ignored.
-using BundleKey = PacketTrace;
+// BundleKey encodes a PacketTrace minus timestamp and length. The key should
+// match many packets over time for interning. For convenience, sport/dport
+// are parsed here as either local/remote port or icmp type/code.
+struct BundleKey {
+  explicit BundleKey(const PacketTrace& pkt);
 
-// BundleKeys are hashed using all fields except timestamp/length.
+  uint32_t ifindex;
+  uint32_t uid;
+  uint32_t tag;
+
+  bool egress;
+  uint8_t ipProto;
+  uint8_t ipVersion;
+
+  std::optional<uint8_t> tcpFlags;
+  std::optional<uint16_t> localPort;
+  std::optional<uint16_t> remotePort;
+  std::optional<uint8_t> icmpType;
+  std::optional<uint8_t> icmpCode;
+};
+
+// BundleKeys are hashed using a simple hash combine.
 struct BundleHash {
   std::size_t operator()(const BundleKey& a) const;
 };
 
-// BundleKeys are equal if all fields except timestamp/length are equal.
+// BundleKeys are equal if all fields are equal.
 struct BundleEq {
   bool operator()(const BundleKey& a, const BundleKey& b) const;
 };
@@ -84,13 +102,13 @@
              NetworkTraceHandler::TraceContext& ctx);
 
  private:
-  // Convert a PacketTrace into a Perfetto trace packet.
-  void Fill(const PacketTrace& src,
+  // Fills in contextual information from a bundle without interning.
+  void Fill(const BundleKey& src,
             ::perfetto::protos::pbzero::NetworkPacketEvent* event);
 
   // Fills in contextual information either inline or via interning.
   ::perfetto::protos::pbzero::NetworkPacketBundle* FillWithInterning(
-      NetworkTraceState* state, const BundleKey& key,
+      NetworkTraceState* state, const BundleKey& src,
       ::perfetto::protos::pbzero::TracePacket* dst);
 
   static internal::NetworkTracePoller sPoller;
diff --git a/service-t/src/com/android/server/ConnectivityServiceInitializer.java b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
index 624c5df..1ac2f6e 100644
--- a/service-t/src/com/android/server/ConnectivityServiceInitializer.java
+++ b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
@@ -16,7 +16,10 @@
 
 package com.android.server;
 
+import android.annotation.Nullable;
 import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.thread.ThreadNetworkManager;
 import android.util.Log;
 
 import com.android.modules.utils.build.SdkLevel;
@@ -25,7 +28,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;
+import com.android.server.thread.ThreadNetworkService;
 
 /**
  * Connectivity service initializer for core networking. This is called by system server to create
@@ -39,7 +42,7 @@
     private final NsdService mNsdService;
     private final NearbyService mNearbyService;
     private final EthernetServiceImpl mEthernetServiceImpl;
-    private final RemoteAuthService mRemoteAuthService;
+    private final ThreadNetworkService mThreadNetworkService;
 
     public ConnectivityServiceInitializer(Context context) {
         super(context);
@@ -51,7 +54,7 @@
         mConnectivityNative = createConnectivityNativeService(context);
         mNsdService = createNsdService(context);
         mNearbyService = createNearbyService(context);
-        mRemoteAuthService = createRemoteAuthService(context);
+        mThreadNetworkService = createThreadNetworkService(context);
     }
 
     @Override
@@ -88,9 +91,9 @@
                     /* allowIsolated= */ false);
         }
 
-        if (mRemoteAuthService != null) {
-            Log.i(TAG, "Registering " + RemoteAuthService.SERVICE_NAME);
-            publishBinderService(RemoteAuthService.SERVICE_NAME, mRemoteAuthService,
+        if (mThreadNetworkService != null) {
+            Log.i(TAG, "Registering " + ThreadNetworkManager.SERVICE_NAME);
+            publishBinderService(ThreadNetworkManager.SERVICE_NAME, mThreadNetworkService,
                     /* allowIsolated= */ false);
         }
     }
@@ -104,6 +107,10 @@
         if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY && mEthernetServiceImpl != null) {
             mEthernetServiceImpl.start();
         }
+
+        if (mThreadNetworkService != null) {
+            mThreadNetworkService.onBootPhase(phase);
+        }
     }
 
     /**
@@ -148,19 +155,6 @@
         }
     }
 
-    /** 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 " + RemoteAuthService.SERVICE_NAME);
-            return null;
-        }
-    }
-
     /**
      * Return EthernetServiceImpl instance or null if current SDK is lower than T or Ethernet
      * service isn't necessary.
@@ -171,4 +165,25 @@
         }
         return EthernetService.create(context);
     }
+
+    /**
+     * Returns Thread network service instance if supported.
+     * Thread is supported if all of below are satisfied:
+     * 1. the FEATURE_THREAD_NETWORK is available
+     * 2. the SDK level is V+, or SDK level is U and the device is a TV
+     */
+    @Nullable
+    private ThreadNetworkService createThreadNetworkService(final Context context) {
+        final PackageManager pm = context.getPackageManager();
+        if (!pm.hasSystemFeature(ThreadNetworkManager.FEATURE_NAME)) {
+            return null;
+        }
+        if (!SdkLevel.isAtLeastU()) {
+            return null;
+        }
+        if (!SdkLevel.isAtLeastV() && !pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
+            return null;
+        }
+        return new ThreadNetworkService(context);
+    }
 }
diff --git a/service-t/src/com/android/server/NetworkStatsServiceInitializer.java b/service-t/src/com/android/server/NetworkStatsServiceInitializer.java
index 82a4fbd..675e5a1 100644
--- a/service-t/src/com/android/server/NetworkStatsServiceInitializer.java
+++ b/service-t/src/com/android/server/NetworkStatsServiceInitializer.java
@@ -22,6 +22,7 @@
 import android.util.Log;
 
 import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.DeviceConfigUtils;
 import com.android.server.net.NetworkStatsService;
 
 /**
@@ -30,6 +31,8 @@
  */
 public final class NetworkStatsServiceInitializer extends SystemService {
     private static final String TAG = NetworkStatsServiceInitializer.class.getSimpleName();
+    private static final String ENABLE_NETWORK_TRACING = "enable_network_tracing";
+    private final boolean mNetworkTracingFlagEnabled;
     private final NetworkStatsService mStatsService;
 
     public NetworkStatsServiceInitializer(Context context) {
@@ -37,6 +40,8 @@
         // Load JNI libraries used by NetworkStatsService and its dependencies
         System.loadLibrary("service-connectivity");
         mStatsService = maybeCreateNetworkStatsService(context);
+        mNetworkTracingFlagEnabled = DeviceConfigUtils.isTetheringFeatureEnabled(
+            context, ENABLE_NETWORK_TRACING);
     }
 
     @Override
@@ -48,11 +53,10 @@
             TrafficStats.init(getContext());
         }
 
-        // The following code registers the Perfetto Network Trace Handler on non-user builds.
-        // The enhanced tracing is intended to be used for debugging and diagnosing issues. This
-        // is conditional on the build type rather than `isDebuggable` to match the system_server
-        // selinux rules which only allow the Perfetto connection under the same circumstances.
-        if (SdkLevel.isAtLeastU() && !Build.TYPE.equals("user")) {
+        // The following code registers the Perfetto Network Trace Handler. The enhanced tracing
+        // is intended to be used for debugging and diagnosing issues. This is enabled by default
+        // on userdebug/eng builds and flag protected in user builds.
+        if (SdkLevel.isAtLeastU() && (mNetworkTracingFlagEnabled || !Build.TYPE.equals("user"))) {
             Log.i(TAG, "Initializing network tracing hooks");
             NetworkStatsService.nativeInitNetworkTracing();
         }
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index c951e98..93ccb85 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -26,6 +26,7 @@
 import static android.net.nsd.NsdManager.MDNS_SERVICE_EVENT;
 import static android.net.nsd.NsdManager.RESOLVE_SERVICE_SUCCEEDED;
 import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
+
 import static com.android.modules.utils.build.SdkLevel.isAtLeastU;
 import static com.android.networkstack.apishim.ConstantsShim.REGISTER_NSD_OFFLOAD_ENGINE;
 import static com.android.server.connectivity.mdns.MdnsAdvertiser.AdvertiserMetrics;
@@ -35,6 +36,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresApi;
 import android.app.ActivityManager;
 import android.content.Context;
 import android.content.Intent;
@@ -59,6 +61,7 @@
 import android.net.nsd.OffloadServiceInfo;
 import android.net.wifi.WifiManager;
 import android.os.Binder;
+import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
@@ -120,6 +123,7 @@
  *
  * @hide
  */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class NsdService extends INsdManager.Stub {
     private static final String TAG = "NsdService";
     private static final String MDNS_TAG = "mDnsConnector";
@@ -1684,7 +1688,10 @@
         mMdnsSocketProvider = deps.makeMdnsSocketProvider(ctx, handler.getLooper(),
                 LOGGER.forSubComponent("MdnsSocketProvider"), new SocketRequestMonitor());
         // Netlink monitor starts on boot, and intentionally never stopped, to ensure that all
-        // address events are received.
+        // address events are received. When the netlink monitor starts, any IP addresses already
+        // on the interfaces will not be seen. In practice, the network will not connect at boot
+        // time As a result, all the netlink message should be observed if the netlink monitor
+        // starts here.
         handler.post(mMdnsSocketProvider::startNetLinkMonitor);
 
         // NsdService is started after ActivityManager (startOtherServices in SystemServer, vs.
@@ -1703,7 +1710,7 @@
                 mMdnsSocketClient, LOGGER.forSubComponent("MdnsDiscoveryManager"));
         handler.post(() -> mMdnsSocketClient.setCallback(mMdnsDiscoveryManager));
         MdnsFeatureFlags flags = new MdnsFeatureFlags.Builder().setIsMdnsOffloadFeatureEnabled(
-                mDeps.isTetheringFeatureNotChickenedOut(
+                mDeps.isTetheringFeatureNotChickenedOut(mContext,
                         MdnsFeatureFlags.NSD_FORCE_DISABLE_MDNS_OFFLOAD)).build();
         mAdvertiser = deps.makeMdnsAdvertiser(handler.getLooper(), mMdnsSocketProvider,
                 new AdvertiserCallback(), LOGGER.forSubComponent("MdnsAdvertiser"), flags);
@@ -1757,8 +1764,8 @@
         /**
          * @see DeviceConfigUtils#isTetheringFeatureNotChickenedOut
          */
-        public boolean isTetheringFeatureNotChickenedOut(String feature) {
-            return DeviceConfigUtils.isTetheringFeatureNotChickenedOut(feature);
+        public boolean isTetheringFeatureNotChickenedOut(Context context, String feature) {
+            return DeviceConfigUtils.isTetheringFeatureNotChickenedOut(context, feature);
         }
 
         /**
diff --git a/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java b/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
index fa3b646..1582fb6 100644
--- a/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
+++ b/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
@@ -19,6 +19,7 @@
 import static com.android.server.connectivity.mdns.MdnsServiceTypeClient.INVALID_TRANSACTION_ID;
 
 import android.annotation.NonNull;
+import android.os.Build;
 import android.text.TextUtils;
 import android.util.Pair;
 
@@ -220,7 +221,9 @@
             throws IOException {
         DatagramPacket packet = packetWriter.getPacket(address);
         if (expectUnicastResponse) {
-            if (requestSender instanceof MdnsMultinetworkSocketClient) {
+            // MdnsMultinetworkSocketClient is only available on T+
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
+                    && requestSender instanceof MdnsMultinetworkSocketClient) {
                 ((MdnsMultinetworkSocketClient) requestSender).sendPacketRequestingUnicastResponse(
                         packet, socketKey, onlyUseIpv6OnIpv6OnlyNetworks);
             } else {
@@ -228,7 +231,8 @@
                         packet, onlyUseIpv6OnIpv6OnlyNetworks);
             }
         } else {
-            if (requestSender instanceof MdnsMultinetworkSocketClient) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
+                    && requestSender instanceof MdnsMultinetworkSocketClient) {
                 ((MdnsMultinetworkSocketClient) requestSender)
                         .sendPacketRequestingMulticastResponse(
                                 packet, socketKey, onlyUseIpv6OnIpv6OnlyNetworks);
diff --git a/service-t/src/com/android/server/connectivity/mdns/ExecutorProvider.java b/service-t/src/com/android/server/connectivity/mdns/ExecutorProvider.java
index 161669b..5d75b48 100644
--- a/service-t/src/com/android/server/connectivity/mdns/ExecutorProvider.java
+++ b/service-t/src/com/android/server/connectivity/mdns/ExecutorProvider.java
@@ -17,7 +17,8 @@
 package com.android.server.connectivity.mdns;
 
 import android.annotation.NonNull;
-import android.util.ArraySet;
+
+import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.util.Set;
 import java.util.concurrent.ScheduledExecutorService;
@@ -30,7 +31,7 @@
 public class ExecutorProvider {
 
     private final Set<ScheduledExecutorService> serviceTypeClientSchedulerExecutors =
-            new ArraySet<>();
+            MdnsUtils.newSet();
 
     /** Returns a new {@link ScheduledExecutorService} instance. */
     public ScheduledExecutorService newServiceTypeClientSchedulerExecutor() {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
index ce13747..a946bca 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -21,12 +21,14 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresApi;
 import android.net.LinkAddress;
 import android.net.Network;
 import android.net.nsd.NsdManager;
 import android.net.nsd.NsdServiceInfo;
 import android.net.nsd.OffloadEngine;
 import android.net.nsd.OffloadServiceInfo;
+import android.os.Build;
 import android.os.Looper;
 import android.util.ArrayMap;
 import android.util.Log;
@@ -50,6 +52,7 @@
  *
  * All methods except the constructor must be called on the looper thread.
  */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class MdnsAdvertiser {
     private static final String TAG = MdnsAdvertiser.class.getSimpleName();
     static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAnnouncer.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAnnouncer.java
index fd2c32e..5812797 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAnnouncer.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAnnouncer.java
@@ -18,6 +18,8 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresApi;
+import android.os.Build;
 import android.os.Looper;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -31,6 +33,7 @@
  *
  * This allows maintaining other hosts' caches up-to-date. See RFC6762 8.3.
  */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class MdnsAnnouncer extends MdnsPacketRepeater<MdnsAnnouncer.BaseAnnouncementInfo> {
     private static final long ANNOUNCEMENT_INITIAL_DELAY_MS = 1000L;
     @VisibleForTesting
@@ -107,7 +110,7 @@
             @NonNull MdnsReplySender replySender,
             @Nullable PacketRepeaterCallback<BaseAnnouncementInfo> cb,
             @NonNull SharedLog sharedLog) {
-        super(looper, replySender, cb, sharedLog);
+        super(looper, replySender, cb, sharedLog, MdnsAdvertiser.DBG);
     }
 
     // TODO: Notify MdnsRecordRepository that the records were announced for that service ID,
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsConstants.java b/service-t/src/com/android/server/connectivity/mdns/MdnsConstants.java
index 1251170..b83a6a0 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsConstants.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsConstants.java
@@ -19,6 +19,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import java.net.InetAddress;
+import java.net.InetSocketAddress;
 import java.net.UnknownHostException;
 import java.nio.charset.Charset;
 
@@ -42,6 +43,10 @@
     public static final String SUBTYPE_PREFIX = "_";
     private static final String MDNS_IPV4_HOST_ADDRESS = "224.0.0.251";
     private static final String MDNS_IPV6_HOST_ADDRESS = "FF02::FB";
+    public static final InetSocketAddress IPV6_SOCKET_ADDR = new InetSocketAddress(
+            getMdnsIPv6Address(), MDNS_PORT);
+    public static final InetSocketAddress IPV4_SOCKET_ADDR = new InetSocketAddress(
+            getMdnsIPv4Address(), MDNS_PORT);
     private static InetAddress mdnsAddress;
     private MdnsConstants() {
     }
@@ -75,4 +80,4 @@
     public static Charset getUtf8Charset() {
         return UTF_8;
     }
-}
\ No newline at end of file
+}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
index d55098c..24e9fa8 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -35,6 +35,7 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * This class keeps tracking the set of registered {@link MdnsServiceBrowserListener} instances, and
@@ -102,8 +103,12 @@
         }
 
         public void remove(@NonNull MdnsServiceTypeClient client) {
-            final int index = clients.indexOfValue(client);
-            clients.removeAt(index);
+            for (int i = 0; i < clients.size(); ++i) {
+                if (Objects.equals(client, clients.valueAt(i))) {
+                    clients.removeAt(i);
+                    break;
+                }
+            }
         }
 
         public boolean isEmpty() {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
index 6454959..37e9743 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -20,8 +20,10 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresApi;
 import android.net.LinkAddress;
 import android.net.nsd.NsdServiceInfo;
+import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 
@@ -39,6 +41,7 @@
 /**
  * A class that handles advertising services on a {@link MdnsInterfaceSocket} tied to an interface.
  */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class MdnsInterfaceAdvertiser implements MulticastPacketReader.PacketHandler {
     private static final boolean DBG = MdnsAdvertiser.DBG;
     @VisibleForTesting
@@ -158,7 +161,7 @@
                 @NonNull SharedLog sharedLog) {
             return new MdnsReplySender(looper, socket, packetCreationBuffer,
                     sharedLog.forSubComponent(
-                            MdnsReplySender.class.getSimpleName() + "/" + interfaceTag));
+                            MdnsReplySender.class.getSimpleName() + "/" + interfaceTag), DBG);
         }
 
         /** @see MdnsAnnouncer */
@@ -367,7 +370,7 @@
         // happen when the incoming packet has answer records (not a question), so there will be no
         // answer. One exception is simultaneous probe tiebreaking (rfc6762 8.2), in which case the
         // conflicting service is still probing and won't reply either.
-        final MdnsRecordRepository.ReplyInfo answers = mRecordRepository.getReply(packet, src);
+        final MdnsReplyInfo answers = mRecordRepository.getReply(packet, src);
 
         if (answers == null) return;
         mReplySender.queueReply(answers);
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceSocket.java b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceSocket.java
index 534f8d0..63dd703 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceSocket.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceSocket.java
@@ -20,8 +20,10 @@
 import static com.android.server.connectivity.mdns.MdnsSocket.MULTICAST_IPV6_ADDRESS;
 
 import android.annotation.NonNull;
+import android.annotation.RequiresApi;
 import android.net.LinkAddress;
 import android.net.util.SocketUtils;
+import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.ParcelFileDescriptor;
@@ -49,6 +51,7 @@
  * @see MulticastSocket for javadoc of each public method.
  * @see MulticastSocket for javadoc of each public method.
  */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class MdnsInterfaceSocket {
     private static final String TAG = MdnsInterfaceSocket.class.getSimpleName();
     @NonNull private final MulticastSocket mMulticastSocket;
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 2ef7368..4ba6912 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
@@ -20,8 +20,10 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresApi;
 import android.net.LinkAddress;
 import android.net.Network;
+import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 import android.util.ArrayMap;
@@ -40,6 +42,7 @@
  *
  *  * <p>This class is not thread safe.
  */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class MdnsMultinetworkSocketClient implements MdnsSocketClientBase {
     private static final String TAG = MdnsMultinetworkSocketClient.class.getSimpleName();
     private static final boolean DBG = MdnsDiscoveryManager.DBG;
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPacket.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPacket.java
index 27002b9..1fabd49 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPacket.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPacket.java
@@ -18,7 +18,6 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.util.Log;
 
 import java.io.EOFException;
 import java.io.IOException;
@@ -32,6 +31,7 @@
 public class MdnsPacket {
     private static final String TAG = MdnsPacket.class.getSimpleName();
 
+    public final int transactionId;
     public final int flags;
     @NonNull
     public final List<MdnsRecord> questions;
@@ -47,6 +47,15 @@
             @NonNull List<MdnsRecord> answers,
             @NonNull List<MdnsRecord> authorityRecords,
             @NonNull List<MdnsRecord> additionalRecords) {
+        this(0, flags, questions, answers, authorityRecords, additionalRecords);
+    }
+
+    MdnsPacket(int transactionId, int flags,
+            @NonNull List<MdnsRecord> questions,
+            @NonNull List<MdnsRecord> answers,
+            @NonNull List<MdnsRecord> authorityRecords,
+            @NonNull List<MdnsRecord> additionalRecords) {
+        this.transactionId = transactionId;
         this.flags = flags;
         this.questions = Collections.unmodifiableList(questions);
         this.answers = Collections.unmodifiableList(answers);
@@ -71,15 +80,16 @@
      */
     @NonNull
     public static MdnsPacket parse(@NonNull MdnsPacketReader reader) throws ParseException {
+        final int transactionId;
         final int flags;
         try {
-            reader.readUInt16(); // transaction ID (not used)
+            transactionId = reader.readUInt16();
             flags = reader.readUInt16();
         } catch (EOFException e) {
             throw new ParseException(MdnsResponseErrorCode.ERROR_END_OF_FILE,
                     "Reached the end of the mDNS response unexpectedly.", e);
         }
-        return parseRecordsSection(reader, flags);
+        return parseRecordsSection(reader, flags, transactionId);
     }
 
     /**
@@ -87,8 +97,8 @@
      *
      * The records section starts with the questions count, just after the packet flags.
      */
-    public static MdnsPacket parseRecordsSection(@NonNull MdnsPacketReader reader, int flags)
-            throws ParseException {
+    public static MdnsPacket parseRecordsSection(@NonNull MdnsPacketReader reader, int flags,
+            int transactionId) throws ParseException {
         try {
             final int numQuestions = reader.readUInt16();
             final int numAnswers = reader.readUInt16();
@@ -100,7 +110,7 @@
             final ArrayList<MdnsRecord> authority = parseRecords(reader, numAuthority, false);
             final ArrayList<MdnsRecord> additional = parseRecords(reader, numAdditional, false);
 
-            return new MdnsPacket(flags, questions, answers, authority, additional);
+            return new MdnsPacket(transactionId, flags, questions, answers, authority, additional);
         } catch (EOFException e) {
             throw new ParseException(MdnsResponseErrorCode.ERROR_END_OF_FILE,
                     "Reached the end of the mDNS response unexpectedly.", e);
@@ -206,9 +216,6 @@
 
             default: {
                 try {
-                    if (MdnsAdvertiser.DBG) {
-                        Log.i(TAG, "Skipping parsing of record of unhandled type " + type);
-                    }
                     skipMdnsRecord(reader, isQuestion);
                     return null;
                 } catch (IOException e) {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java
index 12ed139..e84cead 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java
@@ -16,11 +16,13 @@
 
 package com.android.server.connectivity.mdns;
 
-import static com.android.server.connectivity.mdns.MdnsRecordRepository.IPV4_ADDR;
-import static com.android.server.connectivity.mdns.MdnsRecordRepository.IPV6_ADDR;
+import static com.android.server.connectivity.mdns.MdnsConstants.IPV4_SOCKET_ADDR;
+import static com.android.server.connectivity.mdns.MdnsConstants.IPV6_SOCKET_ADDR;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresApi;
+import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
@@ -34,10 +36,10 @@
  * A class used to send several packets at given time intervals.
  * @param <T> The type of the request providing packet repeating parameters.
  */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public abstract class MdnsPacketRepeater<T extends MdnsPacketRepeater.Request> {
-    private static final boolean DBG = MdnsAdvertiser.DBG;
     private static final InetSocketAddress[] ALL_ADDRS = new InetSocketAddress[] {
-            IPV4_ADDR, IPV6_ADDR
+            IPV4_SOCKET_ADDR, IPV6_SOCKET_ADDR
     };
 
     @NonNull
@@ -48,6 +50,7 @@
     private final PacketRepeaterCallback<T> mCb;
     @NonNull
     private final SharedLog mSharedLog;
+    private final boolean mEnableDebugLog;
 
     /**
      * Status callback from {@link MdnsPacketRepeater}.
@@ -108,7 +111,7 @@
             }
 
             final MdnsPacket packet = request.getPacket(index);
-            if (DBG) {
+            if (mEnableDebugLog) {
                 mSharedLog.v("Sending packets for iteration " + index + " out of "
                         + request.getNumSends() + " for ID " + msg.what);
             }
@@ -131,7 +134,7 @@
                 // likely not to be available since the device is in deep sleep anyway.
                 final long delay = request.getDelayMs(nextIndex);
                 sendMessageDelayed(obtainMessage(msg.what, nextIndex, 0, request), delay);
-                if (DBG) mSharedLog.v("Scheduled next packet in " + delay + "ms");
+                if (mEnableDebugLog) mSharedLog.v("Scheduled next packet in " + delay + "ms");
             }
 
             // Call onSent after scheduling the next run, to allow the callback to cancel it
@@ -142,15 +145,17 @@
     }
 
     protected MdnsPacketRepeater(@NonNull Looper looper, @NonNull MdnsReplySender replySender,
-            @Nullable PacketRepeaterCallback<T> cb, @NonNull SharedLog sharedLog) {
+            @Nullable PacketRepeaterCallback<T> cb, @NonNull SharedLog sharedLog,
+            boolean enableDebugLog) {
         mHandler = new ProbeHandler(looper);
         mReplySender = replySender;
         mCb = cb;
         mSharedLog = sharedLog;
+        mEnableDebugLog = enableDebugLog;
     }
 
     protected void startSending(int id, @NonNull T request, long initialDelayMs) {
-        if (DBG) {
+        if (mEnableDebugLog) {
             mSharedLog.v("Starting send with id " + id + ", request "
                     + request.getClass().getSimpleName() + ", delay " + initialDelayMs);
         }
@@ -169,7 +174,7 @@
         // all in the handler queue; unless this method is called from a message, but the current
         // message cannot be cancelled.
         if (mHandler.hasMessages(id)) {
-            if (DBG) {
+            if (mEnableDebugLog) {
                 mSharedLog.v("Stopping send on id " + id);
             }
             mHandler.removeMessages(id);
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java b/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
index ba37f32..e88947a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
@@ -17,6 +17,8 @@
 package com.android.server.connectivity.mdns;
 
 import android.annotation.NonNull;
+import android.annotation.RequiresApi;
+import android.os.Build;
 import android.os.Looper;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -33,13 +35,13 @@
  *
  * TODO: implement receiving replies and handling conflicts.
  */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class MdnsProber extends MdnsPacketRepeater<MdnsProber.ProbingInfo> {
     private static final long CONFLICT_RETRY_DELAY_MS = 5_000L;
 
     public MdnsProber(@NonNull Looper looper, @NonNull MdnsReplySender replySender,
-            @NonNull PacketRepeaterCallback<ProbingInfo> cb,
-            @NonNull SharedLog sharedLog) {
-        super(looper, replySender, cb, sharedLog);
+            @NonNull PacketRepeaterCallback<ProbingInfo> cb, @NonNull SharedLog sharedLog) {
+        super(looper, replySender, cb, sharedLog, MdnsAdvertiser.DBG);
     }
 
     /** Probing request to send with {@link MdnsProber}. */
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
index f532372..130ff48 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -16,6 +16,8 @@
 
 package com.android.server.connectivity.mdns;
 
+import static com.android.server.connectivity.mdns.MdnsConstants.IPV4_SOCKET_ADDR;
+import static com.android.server.connectivity.mdns.MdnsConstants.IPV6_SOCKET_ADDR;
 import static com.android.server.connectivity.mdns.MdnsConstants.NO_PACKET;
 
 import android.annotation.NonNull;
@@ -79,11 +81,6 @@
     private static final String[] DNS_SD_SERVICE_TYPE =
             new String[] { "_services", "_dns-sd", "_udp", LOCAL_TLD };
 
-    public static final InetSocketAddress IPV6_ADDR = new InetSocketAddress(
-            MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT);
-    public static final InetSocketAddress IPV4_ADDR = new InetSocketAddress(
-            MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT);
-
     @NonNull
     private final Random mDelayGenerator = new Random();
     // Map of service unique ID -> records for service
@@ -455,44 +452,13 @@
     }
 
     /**
-     * Info about a reply to be sent.
-     */
-    public static class ReplyInfo {
-        @NonNull
-        public final List<MdnsRecord> answers;
-        @NonNull
-        public final List<MdnsRecord> additionalAnswers;
-        public final long sendDelayMs;
-        @NonNull
-        public final InetSocketAddress destination;
-
-        public ReplyInfo(
-                @NonNull List<MdnsRecord> answers,
-                @NonNull List<MdnsRecord> additionalAnswers,
-                long sendDelayMs,
-                @NonNull InetSocketAddress destination) {
-            this.answers = answers;
-            this.additionalAnswers = additionalAnswers;
-            this.sendDelayMs = sendDelayMs;
-            this.destination = destination;
-        }
-
-        @Override
-        public String toString() {
-            return "{ReplyInfo to " + destination + ", answers: " + answers.size()
-                    + ", additionalAnswers: " + additionalAnswers.size()
-                    + ", sendDelayMs " + sendDelayMs + "}";
-        }
-    }
-
-    /**
      * Get the reply to send to an incoming packet.
      *
      * @param packet The incoming packet.
      * @param src The source address of the incoming packet.
      */
     @Nullable
-    public ReplyInfo getReply(MdnsPacket packet, InetSocketAddress src) {
+    public MdnsReplyInfo getReply(MdnsPacket packet, InetSocketAddress src) {
         final long now = SystemClock.elapsedRealtime();
         final boolean replyUnicast = (packet.flags & MdnsConstants.QCLASS_UNICAST) != 0;
         final ArrayList<MdnsRecord> additionalAnswerRecords = new ArrayList<>();
@@ -543,9 +509,9 @@
         if (replyUnicast) {
             dest = src;
         } else if (src.getAddress() instanceof Inet4Address) {
-            dest = IPV4_ADDR;
+            dest = IPV4_SOCKET_ADDR;
         } else {
-            dest = IPV6_ADDR;
+            dest = IPV6_SOCKET_ADDR;
         }
 
         // Build the list of answer records from their RecordInfo
@@ -559,7 +525,7 @@
             answerRecords.add(info.record);
         }
 
-        return new ReplyInfo(answerRecords, additionalAnswerRecords, delayMs, dest);
+        return new MdnsReplyInfo(answerRecords, additionalAnswerRecords, delayMs, dest);
     }
 
     /**
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsReplyInfo.java b/service-t/src/com/android/server/connectivity/mdns/MdnsReplyInfo.java
new file mode 100644
index 0000000..ce61b54
--- /dev/null
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsReplyInfo.java
@@ -0,0 +1,53 @@
+/*
+ * 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.connectivity.mdns;
+
+import android.annotation.NonNull;
+
+import java.net.InetSocketAddress;
+import java.util.List;
+
+/**
+ * Info about a mDNS reply to be sent.
+ */
+public final class MdnsReplyInfo {
+    @NonNull
+    public final List<MdnsRecord> answers;
+    @NonNull
+    public final List<MdnsRecord> additionalAnswers;
+    public final long sendDelayMs;
+    @NonNull
+    public final InetSocketAddress destination;
+
+    public MdnsReplyInfo(
+            @NonNull List<MdnsRecord> answers,
+            @NonNull List<MdnsRecord> additionalAnswers,
+            long sendDelayMs,
+            @NonNull InetSocketAddress destination) {
+        this.answers = answers;
+        this.additionalAnswers = additionalAnswers;
+        this.sendDelayMs = sendDelayMs;
+        this.destination = destination;
+    }
+
+    @Override
+    public String toString() {
+        return "{MdnsReplyInfo to " + destination + ", answers: " + answers.size()
+                + ", additionalAnswers: " + additionalAnswers.size()
+                + ", sendDelayMs " + sendDelayMs + "}";
+    }
+}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
index 71057fb..abf5d99 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
@@ -19,12 +19,13 @@
 import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
 
 import android.annotation.NonNull;
+import android.annotation.RequiresApi;
+import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
 
 import com.android.net.module.util.SharedLog;
-import com.android.server.connectivity.mdns.MdnsRecordRepository.ReplyInfo;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.io.IOException;
@@ -41,8 +42,8 @@
  *
  * TODO: implement sending after a delay, combining queued replies and duplicate answer suppression
  */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class MdnsReplySender {
-    private static final boolean DBG = MdnsAdvertiser.DBG;
     private static final int MSG_SEND = 1;
     private static final int PACKET_NOT_SENT = 0;
     private static final int PACKET_SENT = 1;
@@ -55,24 +56,27 @@
     private final byte[] mPacketCreationBuffer;
     @NonNull
     private final SharedLog mSharedLog;
+    private final boolean mEnableDebugLog;
 
     public MdnsReplySender(@NonNull Looper looper, @NonNull MdnsInterfaceSocket socket,
-            @NonNull byte[] packetCreationBuffer, @NonNull SharedLog sharedLog) {
+            @NonNull byte[] packetCreationBuffer, @NonNull SharedLog sharedLog,
+            boolean enableDebugLog) {
         mHandler = new SendHandler(looper);
         mSocket = socket;
         mPacketCreationBuffer = packetCreationBuffer;
         mSharedLog = sharedLog;
+        mEnableDebugLog = enableDebugLog;
     }
 
     /**
      * Queue a reply to be sent when its send delay expires.
      */
-    public void queueReply(@NonNull ReplyInfo reply) {
+    public void queueReply(@NonNull MdnsReplyInfo reply) {
         ensureRunningOnHandlerThread(mHandler);
         // TODO: implement response aggregation (RFC 6762 6.4)
         mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SEND, reply), reply.sendDelayMs);
 
-        if (DBG) {
+        if (mEnableDebugLog) {
             mSharedLog.v("Scheduling " + reply);
         }
     }
@@ -115,8 +119,8 @@
 
         @Override
         public void handleMessage(@NonNull Message msg) {
-            final ReplyInfo replyInfo = (ReplyInfo) msg.obj;
-            if (DBG) mSharedLog.v("Sending " + replyInfo);
+            final MdnsReplyInfo replyInfo = (MdnsReplyInfo) msg.obj;
+            if (mEnableDebugLog) mSharedLog.v("Sending " + replyInfo);
 
             final int flags = 0x8400; // Response, authoritative (rfc6762 18.4)
             final MdnsPacket packet = new MdnsPacket(flags,
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java b/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
index 2f10bde..050913f 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
@@ -20,7 +20,6 @@
 import android.annotation.Nullable;
 import android.net.Network;
 import android.util.ArrayMap;
-import android.util.ArraySet;
 import android.util.Pair;
 
 import com.android.server.connectivity.mdns.util.MdnsUtils;
@@ -29,6 +28,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.Set;
 
 /** A class that decodes mDNS responses from UDP packets. */
 public class MdnsResponseDecoder {
@@ -90,14 +90,14 @@
 
         final MdnsPacket mdnsPacket;
         try {
-            reader.readUInt16(); // transaction ID (not used)
+            final int transactionId = reader.readUInt16();
             int flags = reader.readUInt16();
             if ((flags & MdnsConstants.FLAGS_RESPONSE_MASK) != MdnsConstants.FLAGS_RESPONSE) {
                 throw new MdnsPacket.ParseException(
                         MdnsResponseErrorCode.ERROR_NOT_RESPONSE_MESSAGE, "Not a response", null);
             }
 
-            mdnsPacket = MdnsPacket.parseRecordsSection(reader, flags);
+            mdnsPacket = MdnsPacket.parseRecordsSection(reader, flags, transactionId);
             if (mdnsPacket.answers.size() < 1) {
                 throw new MdnsPacket.ParseException(
                         MdnsResponseErrorCode.ERROR_NO_ANSWERS, "Response has no answers",
@@ -125,7 +125,7 @@
      *                     2) A copy of the original responses with some of them have records
      *                     update or only contains receive time updated.
      */
-    public Pair<ArraySet<MdnsResponse>, ArrayList<MdnsResponse>> augmentResponses(
+    public Pair<Set<MdnsResponse>, ArrayList<MdnsResponse>> augmentResponses(
             @NonNull MdnsPacket mdnsPacket,
             @NonNull Collection<MdnsResponse> existingResponses, int interfaceIndex,
             @Nullable Network network) {
@@ -136,7 +136,7 @@
         records.addAll(mdnsPacket.authorityRecords);
         records.addAll(mdnsPacket.additionalRecords);
 
-        final ArraySet<MdnsResponse> modified = new ArraySet<>();
+        final Set<MdnsResponse> modified = MdnsUtils.newSet();
         final ArrayList<MdnsResponse> responses = new ArrayList<>(existingResponses.size());
         final ArrayMap<MdnsResponse, MdnsResponse> augmentedToOriginal = new ArrayMap<>();
         for (MdnsResponse existing : existingResponses) {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSearchOptions.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSearchOptions.java
index f09596d..63835d9 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSearchOptions.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSearchOptions.java
@@ -22,7 +22,8 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.text.TextUtils;
-import android.util.ArraySet;
+
+import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -46,11 +47,11 @@
                 public MdnsSearchOptions createFromParcel(Parcel source) {
                     return new MdnsSearchOptions(
                             source.createStringArrayList(),
-                            source.readBoolean(),
-                            source.readBoolean(),
+                            source.readInt() == 1,
+                            source.readInt() == 1,
                             source.readParcelable(null),
                             source.readString(),
-                            source.readBoolean(),
+                            source.readInt() == 1,
                             source.readInt());
                 }
 
@@ -165,11 +166,11 @@
     @Override
     public void writeToParcel(Parcel out, int flags) {
         out.writeStringList(subtypes);
-        out.writeBoolean(isPassiveMode);
-        out.writeBoolean(removeExpiredService);
+        out.writeInt(isPassiveMode ? 1 : 0);
+        out.writeInt(removeExpiredService ? 1 : 0);
         out.writeParcelable(mNetwork, 0);
         out.writeString(resolveInstanceName);
-        out.writeBoolean(onlyUseIpv6OnIpv6OnlyNetworks);
+        out.writeInt(onlyUseIpv6OnIpv6OnlyNetworks ? 1 : 0);
         out.writeInt(numOfQueriesBeforeBackoff);
     }
 
@@ -184,7 +185,7 @@
         private String resolveInstanceName;
 
         private Builder() {
-            subtypes = new ArraySet<>();
+            subtypes = MdnsUtils.newSet();
         }
 
         /**
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
index 861d8d1..bbe8f4c 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -27,7 +27,6 @@
 import android.os.Message;
 import android.text.TextUtils;
 import android.util.ArrayMap;
-import android.util.ArraySet;
 import android.util.Pair;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -42,6 +41,7 @@
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.ScheduledExecutorService;
 
 /**
@@ -414,11 +414,11 @@
                 currentList.add(additionalResponse);
             }
         }
-        final Pair<ArraySet<MdnsResponse>, ArrayList<MdnsResponse>> augmentedResult =
+        final Pair<Set<MdnsResponse>, ArrayList<MdnsResponse>> augmentedResult =
                 responseDecoder.augmentResponses(packet, currentList,
                         socketKey.getInterfaceIndex(), socketKey.getNetwork());
 
-        final ArraySet<MdnsResponse> modifiedResponse = augmentedResult.first;
+        final Set<MdnsResponse> modifiedResponse = augmentedResult.first;
         final ArrayList<MdnsResponse> allResponses = augmentedResult.second;
 
         for (MdnsResponse response : allResponses) {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java
index 23c5a4d..5c9ec09 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java
@@ -19,12 +19,12 @@
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
-
 import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
 import static com.android.server.connectivity.mdns.util.MdnsUtils.isNetworkMatched;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresApi;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -41,6 +41,7 @@
 import android.net.wifi.p2p.WifiP2pGroup;
 import android.net.wifi.p2p.WifiP2pInfo;
 import android.net.wifi.p2p.WifiP2pManager;
+import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 import android.util.ArrayMap;
@@ -67,6 +68,7 @@
  * to their default value (0, false or null).
  *
  */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class MdnsSocketProvider {
     private static final String TAG = MdnsSocketProvider.class.getSimpleName();
     private static final boolean DBG = MdnsDiscoveryManager.DBG;
diff --git a/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java b/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java
index 63119ac..3cd77a4 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java
@@ -22,9 +22,9 @@
 import android.os.Handler;
 import android.os.ParcelFileDescriptor;
 import android.system.Os;
-import android.util.ArraySet;
 
 import com.android.net.module.util.FdEventsReader;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.io.FileDescriptor;
 import java.net.InetSocketAddress;
@@ -39,7 +39,7 @@
     @NonNull
     private final Handler mHandler;
     @NonNull
-    private final Set<PacketHandler> mPacketHandlers = new ArraySet<>();
+    private final Set<PacketHandler> mPacketHandlers = MdnsUtils.newSet();
 
     interface PacketHandler {
         void handlePacket(byte[] recvbuf, int length, InetSocketAddress src);
diff --git a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
index c1c9c42..d0f3d9a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
+++ b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.net.Network;
+import android.os.Build;
 import android.os.Handler;
 import android.os.SystemClock;
 import android.util.ArraySet;
@@ -34,6 +35,8 @@
 import java.nio.charset.Charset;
 import java.nio.charset.CharsetEncoder;
 import java.nio.charset.StandardCharsets;
+import java.util.HashSet;
+import java.util.Set;
 
 /**
  * Mdns utility functions.
@@ -58,6 +61,17 @@
     }
 
     /**
+     * Create a ArraySet or HashSet based on the sdk version.
+     */
+    public static <Type> Set<Type> newSet() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            return new ArraySet<>();
+        } else {
+            return new HashSet<>();
+        }
+    }
+
+    /**
      * Convert the array of labels to DNS case-insensitive lowercase.
      */
     public static String[] toDnsLabelsLowerCase(@NonNull String[] labels) {
@@ -142,7 +156,7 @@
 
     /*** Check whether the target network matches any of the current networks */
     public static boolean isAnyNetworkMatched(@Nullable Network targetNetwork,
-            ArraySet<Network> currentNetworks) {
+            Set<Network> currentNetworks) {
         if (targetNetwork == null) {
             return !currentNetworks.isEmpty();
         }
@@ -175,7 +189,7 @@
         // TODO: support packets over size (send in multiple packets with TC bit set)
         final MdnsPacketWriter writer = new MdnsPacketWriter(packetCreationBuffer);
 
-        writer.writeUInt16(0); // Transaction ID (advertisement: 0)
+        writer.writeUInt16(packet.transactionId); // Transaction ID (advertisement: 0)
         writer.writeUInt16(packet.flags); // Response, authoritative (rfc6762 18.4)
         writer.writeUInt16(packet.questions.size()); // questions count
         writer.writeUInt16(packet.answers.size()); // answers count
diff --git a/service/libconnectivity/include/connectivity_native.h b/service/libconnectivity/include/connectivity_native.h
index 5a2509a..f4676a9 100644
--- a/service/libconnectivity/include/connectivity_native.h
+++ b/service/libconnectivity/include/connectivity_native.h
@@ -78,7 +78,7 @@
  * @param count Pointer to the size of the ports array; the value will be set to the total number of
  *              blocked ports, which may be larger than the ports array that was filled.
  */
-int AConnectivityNative_getPortsBlockedForBind(in_port_t *ports, size_t *count)
+int AConnectivityNative_getPortsBlockedForBind(in_port_t* _Nonnull ports, size_t* _Nonnull count)
     __INTRODUCED_IN(__ANDROID_API_U__);
 
 __END_DECLS
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index 4b24aaf..6a34a24 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -254,7 +254,7 @@
         if (sInitialized) return;
         if (sEnableJavaBpfMap == null) {
             sEnableJavaBpfMap = SdkLevel.isAtLeastU() ||
-                    DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
+                    DeviceConfigUtils.isTetheringFeatureNotChickenedOut(context,
                             BPF_NET_MAPS_FORCE_DISABLE_JAVA_BPF_MAP);
         }
         Log.d(TAG, "BpfNetMaps is initialized with sEnableJavaBpfMap=" + sEnableJavaBpfMap);
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index aa82559..986d58b 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -1317,6 +1317,10 @@
             return SdkLevel.isAtLeastU();
         }
 
+        public boolean isAtLeastV() {
+            return SdkLevel.isAtLeastV();
+        }
+
         /**
          * Get system properties to use in ConnectivityService.
          */
@@ -1430,7 +1434,7 @@
         public CarrierPrivilegeAuthenticator makeCarrierPrivilegeAuthenticator(
                 @NonNull final Context context, @NonNull final TelephonyManager tm) {
             if (isAtLeastT()) {
-                return new CarrierPrivilegeAuthenticator(context, tm);
+                return new CarrierPrivilegeAuthenticator(context, this, tm);
             } else {
                 return null;
             }
@@ -4410,7 +4414,7 @@
                 updateCapabilitiesForNetwork(nai);
             } else if (portalChanged) {
                 if (portal && ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_AVOID
-                        == getCaptivePortalMode()) {
+                        == getCaptivePortalMode(nai)) {
                     if (DBG) log("Avoiding captive portal network: " + nai.toShortString());
                     nai.onPreventAutomaticReconnect();
                     teardownUnneededNetwork(nai);
@@ -4446,7 +4450,13 @@
             }
         }
 
-        private int getCaptivePortalMode() {
+        private int getCaptivePortalMode(@NonNull NetworkAgentInfo nai) {
+            if (nai.networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) &&
+                    mContext.getPackageManager().hasSystemFeature(FEATURE_WATCH)) {
+                // Do not avoid captive portal when network is wear proxy.
+                return ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_PROMPT;
+            }
+
             return Settings.Global.getInt(mContext.getContentResolver(),
                     ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE,
                     ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_PROMPT);
@@ -10619,6 +10629,16 @@
                 err.getFileDescriptor(), args);
     }
 
+    private Boolean parseBooleanArgument(final String arg) {
+        if ("true".equals(arg)) {
+            return true;
+        } else if ("false".equals(arg)) {
+            return false;
+        } else {
+            return null;
+        }
+    }
+
     private class ShellCmd extends BasicShellCommandHandler {
         @Override
         public int onCommand(String cmd) {
@@ -10648,6 +10668,54 @@
                             onHelp();
                             return -1;
                         }
+                    case "set-chain3-enabled": {
+                        final Boolean enabled = parseBooleanArgument(getNextArg());
+                        if (null == enabled) {
+                            onHelp();
+                            return -1;
+                        }
+                        Log.i(TAG, (enabled ? "En" : "Dis") + "abled FIREWALL_CHAIN_OEM_DENY_3");
+                        setFirewallChainEnabled(ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3,
+                                enabled);
+                        return 0;
+                    }
+                    case "get-chain3-enabled": {
+                        final boolean chainEnabled = getFirewallChainEnabled(
+                                ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3);
+                        pw.println("chain:" + (chainEnabled ? "enabled" : "disabled"));
+                        return 0;
+                    }
+                    case "set-package-networking-enabled": {
+                        final Boolean enabled = parseBooleanArgument(getNextArg());
+                        final String packageName = getNextArg();
+                        if (null == enabled || null == packageName) {
+                            onHelp();
+                            return -1;
+                        }
+                        // Throws NameNotFound if the package doesn't exist.
+                        final int appId = setPackageFirewallRule(
+                                ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3,
+                                packageName, enabled ? FIREWALL_RULE_DEFAULT : FIREWALL_RULE_DENY);
+                        final String msg = (enabled ? "Enabled" : "Disabled")
+                                + " networking for " + packageName + ", appId " + appId;
+                        Log.i(TAG, msg);
+                        pw.println(msg);
+                        return 0;
+                    }
+                    case "get-package-networking-enabled": {
+                        final String packageName = getNextArg();
+                        final int rule = getPackageFirewallRule(
+                                ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3, packageName);
+                        if (FIREWALL_RULE_ALLOW == rule || FIREWALL_RULE_DEFAULT == rule) {
+                            pw.println(packageName + ":" + "allow");
+                        } else if (FIREWALL_RULE_DENY == rule) {
+                            pw.println(packageName + ":" + "deny");
+                        } else {
+                            throw new IllegalStateException("Unknown rule " + rule + " for package "
+                                    + packageName);
+                        }
+                        return 0;
+                    }
                     case "reevaluate":
                         // Usage : adb shell cmd connectivity reevaluate <netId>
                         // If netId is omitted, then reevaluate the default network
@@ -10688,6 +10756,15 @@
             pw.println("    Turn airplane mode on or off.");
             pw.println("  airplane-mode");
             pw.println("    Get airplane mode.");
+            pw.println("  set-chain3-enabled [true|false]");
+            pw.println("    Enable or disable FIREWALL_CHAIN_OEM_DENY_3 for debugging.");
+            pw.println("  get-chain3-enabled");
+            pw.println("    Returns whether FIREWALL_CHAIN_OEM_DENY_3 is enabled.");
+            pw.println("  set-package-networking-enabled [true|false] [package name]");
+            pw.println("    Set the deny bit in FIREWALL_CHAIN_OEM_DENY_3 to package. This has\n"
+                    + "    no effect if the chain is disabled.");
+            pw.println("  get-package-networking-enabled [package name]");
+            pw.println("    Get the deny bit in FIREWALL_CHAIN_OEM_DENY_3 for package.");
         }
     }
 
@@ -11358,7 +11435,7 @@
         public void onInterfaceLinkStateChanged(@NonNull String iface, boolean up) {
             mHandler.post(() -> {
                 for (NetworkAgentInfo nai : mNetworkAgentInfos) {
-                    nai.clatd.interfaceLinkStateChanged(iface, up);
+                    nai.clatd.handleInterfaceLinkStateChanged(iface, up);
                 }
             });
         }
@@ -11367,7 +11444,7 @@
         public void onInterfaceRemoved(@NonNull String iface) {
             mHandler.post(() -> {
                 for (NetworkAgentInfo nai : mNetworkAgentInfos) {
-                    nai.clatd.interfaceRemoved(iface);
+                    nai.clatd.handleInterfaceRemoved(iface);
                 }
             });
         }
@@ -11391,7 +11468,8 @@
         // If there is no default network, default network is considered active to keep the existing
         // behavior. Initial value is used until first connect to the default network.
         private volatile boolean mIsDefaultNetworkActive = true;
-        private final ArrayMap<String, IdleTimerParams> mActiveIdleTimers = new ArrayMap<>();
+        // Key is netId. Value is configured idle timer information.
+        private final SparseArray<IdleTimerParams> mActiveIdleTimers = new SparseArray<>();
 
         private static class IdleTimerParams {
             public final int timeout;
@@ -11419,7 +11497,7 @@
 
         public void handleReportNetworkActivity(NetworkActivityParams activityParams) {
             ensureRunningOnConnectivityServiceThread();
-            if (mActiveIdleTimers.isEmpty()) {
+            if (mActiveIdleTimers.size() == 0) {
                 // This activity change is not for the current default network.
                 // This can happen if netd callback post activity change event message but
                 // the default network is lost before processing this message.
@@ -11495,6 +11573,7 @@
          */
         private boolean setupDataActivityTracking(NetworkAgentInfo networkAgent) {
             final String iface = networkAgent.linkProperties.getInterfaceName();
+            final int netId = networkAgent.network().netId;
 
             final int timeout;
             final int type;
@@ -11519,7 +11598,7 @@
 
             if (timeout > 0 && iface != null) {
                 try {
-                    mActiveIdleTimers.put(iface, new IdleTimerParams(timeout, type));
+                    mActiveIdleTimers.put(netId, new IdleTimerParams(timeout, type));
                     mNetd.idletimerAddInterface(iface, timeout, Integer.toString(type));
                     return true;
                 } catch (Exception e) {
@@ -11535,6 +11614,7 @@
          */
         private void removeDataActivityTracking(NetworkAgentInfo networkAgent) {
             final String iface = networkAgent.linkProperties.getInterfaceName();
+            final int netId = networkAgent.network().netId;
             final NetworkCapabilities caps = networkAgent.networkCapabilities;
 
             if (iface == null) return;
@@ -11550,11 +11630,12 @@
 
             try {
                 updateRadioPowerState(false /* isActive */, type);
-                final IdleTimerParams params = mActiveIdleTimers.remove(iface);
+                final IdleTimerParams params = mActiveIdleTimers.get(netId);
                 if (params == null) {
                     // IdleTimer is not added if the configured timeout is 0 or negative value
                     return;
                 }
+                mActiveIdleTimers.remove(netId);
                 // The call fails silently if no idle timer setup for this interface
                 mNetd.idletimerRemoveInterface(iface, params.timeout,
                         Integer.toString(params.transportType));
@@ -11625,9 +11706,9 @@
             pw.print("mIsDefaultNetworkActive="); pw.println(mIsDefaultNetworkActive);
             pw.println("Idle timers:");
             try {
-                for (Map.Entry<String, IdleTimerParams> ent : mActiveIdleTimers.entrySet()) {
-                    pw.print("  "); pw.print(ent.getKey()); pw.println(":");
-                    final IdleTimerParams params = ent.getValue();
+                for (int i = 0; i < mActiveIdleTimers.size(); i++) {
+                    pw.print("  "); pw.print(mActiveIdleTimers.keyAt(i)); pw.println(":");
+                    final IdleTimerParams params = mActiveIdleTimers.valueAt(i);
                     pw.print("    timeout="); pw.print(params.timeout);
                     pw.print(" type="); pw.println(params.transportType);
                 }
@@ -12423,6 +12504,21 @@
         }
     }
 
+    private int setPackageFirewallRule(final int chain, final String packageName, final int rule)
+            throws PackageManager.NameNotFoundException {
+        final PackageManager pm = mContext.getPackageManager();
+        final int appId = UserHandle.getAppId(pm.getPackageUid(packageName, 0 /* flags */));
+        if (appId < Process.FIRST_APPLICATION_UID) {
+            throw new RuntimeException("Can't set package firewall rule for system app "
+                    + packageName + " with appId " + appId);
+        }
+        for (final UserHandle uh : mUserManager.getUserHandles(false /* excludeDying */)) {
+            final int uid = uh.getUid(appId);
+            setUidFirewallRule(chain, uid, rule);
+        }
+        return appId;
+    }
+
     @Override
     public void setUidFirewallRule(final int chain, final int uid, final int rule) {
         enforceNetworkStackOrSettingsPermission();
@@ -12441,6 +12537,13 @@
         }
     }
 
+    private int getPackageFirewallRule(final int chain, final String packageName)
+            throws PackageManager.NameNotFoundException {
+        final PackageManager pm = mContext.getPackageManager();
+        final int appId = UserHandle.getAppId(pm.getPackageUid(packageName, 0 /* flags */));
+        return getUidFirewallRule(chain, appId);
+    }
+
     @Override
     public int getUidFirewallRule(final int chain, final int uid) {
         enforceNetworkStackOrSettingsPermission();
diff --git a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
index 3befcfa..11345d3 100644
--- a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
+++ b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
@@ -974,7 +974,7 @@
          * @return whether the feature is enabled
          */
         public boolean isTetheringFeatureNotChickenedOut(@NonNull final String name) {
-            return DeviceConfigUtils.isTetheringFeatureNotChickenedOut(name);
+            return DeviceConfigUtils.isTetheringFeatureNotChickenedOut(mContext, name);
         }
 
         /**
diff --git a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
index 4325763..ab7b1a7 100644
--- a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
+++ b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
@@ -16,10 +16,12 @@
 
 package com.android.server.connectivity;
 
-import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 
+import static com.android.server.connectivity.ConnectivityFlags.CARRIER_SERVICE_CHANGED_USE_CALLBACK;
+
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -35,6 +37,7 @@
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.util.Log;
+import android.util.SparseIntArray;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -43,6 +46,7 @@
 import com.android.networkstack.apishim.common.TelephonyManagerShim;
 import com.android.networkstack.apishim.common.TelephonyManagerShim.CarrierPrivilegesListenerShim;
 import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
+import com.android.server.ConnectivityService;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -54,7 +58,7 @@
  * carrier privileged app that provides the carrier config
  * @hide
  */
-public class CarrierPrivilegeAuthenticator extends BroadcastReceiver {
+public class CarrierPrivilegeAuthenticator {
     private static final String TAG = CarrierPrivilegeAuthenticator.class.getSimpleName();
     private static final boolean DBG = true;
 
@@ -63,100 +67,100 @@
     private final TelephonyManagerShim mTelephonyManagerShim;
     private final TelephonyManager mTelephonyManager;
     @GuardedBy("mLock")
-    private int[] mCarrierServiceUid;
+    private final SparseIntArray mCarrierServiceUid = new SparseIntArray(2 /* initialCapacity */);
     @GuardedBy("mLock")
     private int mModemCount = 0;
     private final Object mLock = new Object();
-    private final HandlerThread mThread;
     private final Handler mHandler;
     @NonNull
-    private final List<CarrierPrivilegesListenerShim> mCarrierPrivilegesChangedListeners =
-            new ArrayList<>();
+    private final List<PrivilegeListener> mCarrierPrivilegesChangedListeners = new ArrayList<>();
+    private final boolean mUseCallbacksForServiceChanged;
 
     public CarrierPrivilegeAuthenticator(@NonNull final Context c,
+            @NonNull final ConnectivityService.Dependencies deps,
             @NonNull final TelephonyManager t,
-            @NonNull final TelephonyManagerShimImpl telephonyManagerShim) {
+            @NonNull final TelephonyManagerShim telephonyManagerShim) {
         mContext = c;
         mTelephonyManager = t;
         mTelephonyManagerShim = telephonyManagerShim;
-        mThread = new HandlerThread(TAG);
-        mThread.start();
-        mHandler = new Handler(mThread.getLooper()) {};
+        final HandlerThread thread = new HandlerThread(TAG);
+        thread.start();
+        mHandler = new Handler(thread.getLooper());
+        mUseCallbacksForServiceChanged = deps.isFeatureEnabled(
+                c, CARRIER_SERVICE_CHANGED_USE_CALLBACK);
+        final IntentFilter filter = new IntentFilter();
+        filter.addAction(TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED);
         synchronized (mLock) {
-            mModemCount = mTelephonyManager.getActiveModemCount();
-            registerForCarrierChanges();
-            updateCarrierServiceUid();
+            // Never unregistered because the system server never stops
+            c.registerReceiver(new BroadcastReceiver() {
+                @Override
+                public void onReceive(final Context context, final Intent intent) {
+                    switch (intent.getAction()) {
+                        case TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED:
+                            simConfigChanged();
+                            break;
+                        default:
+                            Log.d(TAG, "Unknown intent received, action: " + intent.getAction());
+                    }
+                }
+            }, filter, null, mHandler);
+            simConfigChanged();
         }
     }
 
     public CarrierPrivilegeAuthenticator(@NonNull final Context c,
+            @NonNull final ConnectivityService.Dependencies deps,
             @NonNull final TelephonyManager t) {
-        mContext = c;
-        mTelephonyManager = t;
-        mTelephonyManagerShim = TelephonyManagerShimImpl.newInstance(mTelephonyManager);
-        mThread = new HandlerThread(TAG);
-        mThread.start();
-        mHandler = new Handler(mThread.getLooper()) {};
+        this(c, deps, t, TelephonyManagerShimImpl.newInstance(t));
+    }
+
+    private void simConfigChanged() {
         synchronized (mLock) {
+            unregisterCarrierPrivilegesListeners();
             mModemCount = mTelephonyManager.getActiveModemCount();
-            registerForCarrierChanges();
+            registerCarrierPrivilegesListeners(mModemCount);
+            if (!mUseCallbacksForServiceChanged) updateCarrierServiceUid();
+        }
+    }
+
+    private class PrivilegeListener implements CarrierPrivilegesListenerShim {
+        public final int mLogicalSlot;
+        PrivilegeListener(final int logicalSlot) {
+            mLogicalSlot = logicalSlot;
+        }
+
+        @Override public void onCarrierPrivilegesChanged(
+                @NonNull List<String> privilegedPackageNames,
+                @NonNull int[] privilegedUids) {
+            if (mUseCallbacksForServiceChanged) return;
+            // Re-trigger the synchronous check (which is also very cheap due
+            // to caching in CarrierPrivilegesTracker). This allows consistency
+            // with the onSubscriptionsChangedListener and broadcasts.
             updateCarrierServiceUid();
         }
-    }
 
-    /**
-     * Broadcast receiver for ACTION_MULTI_SIM_CONFIG_CHANGED
-     *
-     * <p>The broadcast receiver is registered with mHandler
-     */
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        switch (intent.getAction()) {
-            case TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED:
-                handleActionMultiSimConfigChanged(context, intent);
-                break;
-            default:
-                Log.d(TAG, "Unknown intent received with action: " + intent.getAction());
+        @Override
+        public void onCarrierServiceChanged(@Nullable final String carrierServicePackageName,
+                final int carrierServiceUid) {
+            if (!mUseCallbacksForServiceChanged) {
+                // Re-trigger the synchronous check (which is also very cheap due
+                // to caching in CarrierPrivilegesTracker). This allows consistency
+                // with the onSubscriptionsChangedListener and broadcasts.
+                updateCarrierServiceUid();
+                return;
+            }
+            synchronized (mLock) {
+                mCarrierServiceUid.put(mLogicalSlot, carrierServiceUid);
+            }
         }
     }
 
-    private void handleActionMultiSimConfigChanged(Context context, Intent intent) {
-        unregisterCarrierPrivilegesListeners();
-        synchronized (mLock) {
-            mModemCount = mTelephonyManager.getActiveModemCount();
-        }
-        registerCarrierPrivilegesListeners();
-        updateCarrierServiceUid();
-    }
-
-    private void registerForCarrierChanges() {
-        final IntentFilter filter = new IntentFilter();
-        filter.addAction(TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED);
-        mContext.registerReceiver(this, filter, null, mHandler);
-        registerCarrierPrivilegesListeners();
-    }
-
-    private void registerCarrierPrivilegesListeners() {
+    private void registerCarrierPrivilegesListeners(final int modemCount) {
         final HandlerExecutor executor = new HandlerExecutor(mHandler);
-        int modemCount;
-        synchronized (mLock) {
-            modemCount = mModemCount;
-        }
         try {
             for (int i = 0; i < modemCount; i++) {
-                CarrierPrivilegesListenerShim carrierPrivilegesListener =
-                        new CarrierPrivilegesListenerShim() {
-                            @Override
-                            public void onCarrierPrivilegesChanged(
-                                    @NonNull List<String> privilegedPackageNames,
-                                    @NonNull int[] privilegedUids) {
-                                // Re-trigger the synchronous check (which is also very cheap due
-                                // to caching in CarrierPrivilegesTracker). This allows consistency
-                                // with the onSubscriptionsChangedListener and broadcasts.
-                                updateCarrierServiceUid();
-                            }
-                        };
-                addCarrierPrivilegesListener(i, executor, carrierPrivilegesListener);
+                PrivilegeListener carrierPrivilegesListener = new PrivilegeListener(i);
+                addCarrierPrivilegesListener(executor, carrierPrivilegesListener);
                 mCarrierPrivilegesChangedListeners.add(carrierPrivilegesListener);
             }
         } catch (IllegalArgumentException e) {
@@ -164,24 +168,13 @@
         }
     }
 
-    private void addCarrierPrivilegesListener(int logicalSlotIndex, Executor executor,
-            CarrierPrivilegesListenerShim listener) {
-        try {
-            mTelephonyManagerShim.addCarrierPrivilegesListener(
-                    logicalSlotIndex, executor, listener);
-        } catch (UnsupportedApiLevelException unsupportedApiLevelException) {
-            // Should not happen since CarrierPrivilegeAuthenticator is only used on T+
-            Log.e(TAG, "addCarrierPrivilegesListener API is not available");
+    @GuardedBy("mLock")
+    private void unregisterCarrierPrivilegesListeners() {
+        for (PrivilegeListener carrierPrivilegesListener : mCarrierPrivilegesChangedListeners) {
+            removeCarrierPrivilegesListener(carrierPrivilegesListener);
+            mCarrierServiceUid.delete(carrierPrivilegesListener.mLogicalSlot);
         }
-    }
-
-    private void removeCarrierPrivilegesListener(CarrierPrivilegesListenerShim listener) {
-        try {
-            mTelephonyManagerShim.removeCarrierPrivilegesListener(listener);
-        } catch (UnsupportedApiLevelException unsupportedApiLevelException) {
-            // Should not happen since CarrierPrivilegeAuthenticator is only used on T+
-            Log.e(TAG, "removeCarrierPrivilegesListener API is not available");
-        }
+        mCarrierPrivilegesChangedListeners.clear();
     }
 
     private String getCarrierServicePackageNameForLogicalSlot(int logicalSlotIndex) {
@@ -195,14 +188,6 @@
         return null;
     }
 
-    private void unregisterCarrierPrivilegesListeners() {
-        for (CarrierPrivilegesListenerShim carrierPrivilegesListener :
-                mCarrierPrivilegesChangedListeners) {
-            removeCarrierPrivilegesListener(carrierPrivilegesListener);
-        }
-        mCarrierPrivilegesChangedListeners.clear();
-    }
-
     /**
      * Check if a UID is the carrier service app of the subscription ID in the provided capabilities
      *
@@ -233,9 +218,9 @@
     @VisibleForTesting
     void updateCarrierServiceUid() {
         synchronized (mLock) {
-            mCarrierServiceUid = new int[mModemCount];
+            mCarrierServiceUid.clear();
             for (int i = 0; i < mModemCount; i++) {
-                mCarrierServiceUid[i] = getCarrierServicePackageUidForSlot(i);
+                mCarrierServiceUid.put(i, getCarrierServicePackageUidForSlot(i));
             }
         }
     }
@@ -244,11 +229,8 @@
     int getCarrierServiceUidForSubId(int subId) {
         final int slotId = getSlotIndex(subId);
         synchronized (mLock) {
-            if (slotId != SubscriptionManager.INVALID_SIM_SLOT_INDEX && slotId < mModemCount) {
-                return mCarrierServiceUid[slotId];
-            }
+            return mCarrierServiceUid.get(slotId, Process.INVALID_UID);
         }
-        return Process.INVALID_UID;
     }
 
     @VisibleForTesting
@@ -288,4 +270,26 @@
     int getCarrierServicePackageUidForSlot(int slotId) {
         return getUidForPackage(getCarrierServicePackageNameForLogicalSlot(slotId));
     }
+
+    // Helper methods to avoid having to deal with UnsupportedApiLevelException.
+
+    private void addCarrierPrivilegesListener(@NonNull final Executor executor,
+            @NonNull final PrivilegeListener listener) {
+        try {
+            mTelephonyManagerShim.addCarrierPrivilegesListener(listener.mLogicalSlot, executor,
+                    listener);
+        } catch (UnsupportedApiLevelException unsupportedApiLevelException) {
+            // Should not happen since CarrierPrivilegeAuthenticator is only used on T+
+            Log.e(TAG, "addCarrierPrivilegesListener API is not available");
+        }
+    }
+
+    private void removeCarrierPrivilegesListener(PrivilegeListener listener) {
+        try {
+            mTelephonyManagerShim.removeCarrierPrivilegesListener(listener);
+        } catch (UnsupportedApiLevelException unsupportedApiLevelException) {
+            // Should not happen since CarrierPrivilegeAuthenticator is only used on T+
+            Log.e(TAG, "removeCarrierPrivilegesListener API is not available");
+        }
+    }
 }
diff --git a/service/src/com/android/server/connectivity/ClatCoordinator.java b/service/src/com/android/server/connectivity/ClatCoordinator.java
index eb3e7ce..17de146 100644
--- a/service/src/com/android/server/connectivity/ClatCoordinator.java
+++ b/service/src/com/android/server/connectivity/ClatCoordinator.java
@@ -78,7 +78,7 @@
     @VisibleForTesting
     static final int MTU_DELTA = 28;
     @VisibleForTesting
-    static final int CLAT_MAX_MTU = 65536;
+    static final int CLAT_MAX_MTU = 1500 + MTU_DELTA;
 
     // This must match the interface prefix in clatd.c.
     private static final String CLAT_PREFIX = "v4-";
@@ -673,7 +673,7 @@
             throw new IOException("Detect MTU on " + tunIface + " failed: " + e);
         }
         final int mtu = adjustMtu(detectedMtu);
-        Log.i(TAG, "ipv4 mtu is " + mtu);
+        Log.i(TAG, "detected ipv4 mtu of " + detectedMtu + " adjusted to " + mtu);
 
         // Config tun interface mtu, address and bring up.
         try {
diff --git a/service/src/com/android/server/connectivity/ConnectivityFlags.java b/service/src/com/android/server/connectivity/ConnectivityFlags.java
index 9039a14..5aac8f1 100644
--- a/service/src/com/android/server/connectivity/ConnectivityFlags.java
+++ b/service/src/com/android/server/connectivity/ConnectivityFlags.java
@@ -33,6 +33,10 @@
     public static final String NO_REMATCH_ALL_REQUESTS_ON_REGISTER =
             "no_rematch_all_requests_on_register";
 
+    @VisibleForTesting
+    public static final String CARRIER_SERVICE_CHANGED_USE_CALLBACK =
+            "carrier_service_changed_use_callback_version";
+
     private boolean mNoRematchAllRequestsOnRegister;
 
     /**
diff --git a/service/src/com/android/server/connectivity/ConnectivityNativeService.java b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
index c1ba40e..e16117b 100644
--- a/service/src/com/android/server/connectivity/ConnectivityNativeService.java
+++ b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
@@ -46,10 +46,6 @@
 
     private static final String TAG = ConnectivityNativeService.class.getSimpleName();
     private static final String CGROUP_PATH = "/sys/fs/cgroup";
-    private static final String V4_PROG_PATH =
-            "/sys/fs/bpf/net_shared/prog_block_bind4_block_port";
-    private static final String V6_PROG_PATH =
-            "/sys/fs/bpf/net_shared/prog_block_bind6_block_port";
     private static final String BLOCKED_PORTS_MAP_PATH =
             "/sys/fs/bpf/net_shared/map_block_blocked_ports_map";
 
@@ -95,7 +91,6 @@
     protected ConnectivityNativeService(final Context context, @NonNull Dependencies deps) {
         mContext = context;
         mBpfBlockedPortsMap = deps.getBlockPortsMap();
-        attachProgram();
     }
 
     @Override
@@ -155,23 +150,4 @@
     public String getInterfaceHash() {
         return this.HASH;
     }
-
-    /**
-     * Attach BPF program
-     */
-    private void attachProgram() {
-        try {
-            BpfUtils.attachProgram(BPF_CGROUP_INET4_BIND, V4_PROG_PATH, CGROUP_PATH, 0);
-        } catch (IOException e) {
-            throw new UnsupportedOperationException("Unable to attach to BPF_CGROUP_INET4_BIND: "
-                    + e);
-        }
-        try {
-            BpfUtils.attachProgram(BPF_CGROUP_INET6_BIND, V6_PROG_PATH, CGROUP_PATH, 0);
-        } catch (IOException e) {
-            throw new UnsupportedOperationException("Unable to attach to BPF_CGROUP_INET6_BIND: "
-                    + e);
-        }
-        Log.d(TAG, "Attached BPF_CGROUP_INET4_BIND and BPF_CGROUP_INET6_BIND programs");
-    }
 }
diff --git a/service/src/com/android/server/connectivity/KeepaliveTracker.java b/service/src/com/android/server/connectivity/KeepaliveTracker.java
index feba821..a51f09f 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.isTetheringFeatureNotChickenedOut(
+                    && DeviceConfigUtils.isTetheringFeatureNotChickenedOut(context,
                             CONFIG_DISABLE_CLAT_ADDRESS_TRANSLATE);
         }
     }
diff --git a/service/src/com/android/server/connectivity/Nat464Xlat.java b/service/src/com/android/server/connectivity/Nat464Xlat.java
index f9e07fd..065922d 100644
--- a/service/src/com/android/server/connectivity/Nat464Xlat.java
+++ b/service/src/com/android/server/connectivity/Nat464Xlat.java
@@ -483,8 +483,9 @@
 
     /**
      * Adds stacked link on base link and transitions to RUNNING state.
+     * Must be called on the handler thread.
      */
-    private void handleInterfaceLinkStateChanged(String iface, boolean up) {
+    public void handleInterfaceLinkStateChanged(String iface, boolean up) {
         // TODO: if we call start(), then stop(), then start() again, and the
         // interfaceLinkStateChanged notification for the first start is delayed past the first
         // stop, then the code becomes out of sync with system state and will behave incorrectly.
@@ -499,6 +500,7 @@
         // Once this code is converted to StateMachine, it will be possible to use deferMessage to
         // ensure it stays in STARTING state until the interfaceLinkStateChanged notification fires,
         // and possibly use a timeout (or provide some guarantees at the lower layer) to address #1.
+        ensureRunningOnHandlerThread();
         if (!isStarting() || !up || !Objects.equals(mIface, iface)) {
             return;
         }
@@ -519,8 +521,10 @@
 
     /**
      * Removes stacked link on base link and transitions to IDLE state.
+     * Must be called on the handler thread.
      */
-    private void handleInterfaceRemoved(String iface) {
+    public void handleInterfaceRemoved(String iface) {
+        ensureRunningOnHandlerThread();
         if (!Objects.equals(mIface, iface)) {
             return;
         }
@@ -536,14 +540,6 @@
         stop();
     }
 
-    public void interfaceLinkStateChanged(String iface, boolean up) {
-        mNetwork.handler().post(() -> { handleInterfaceLinkStateChanged(iface, up); });
-    }
-
-    public void interfaceRemoved(String iface) {
-        mNetwork.handler().post(() -> handleInterfaceRemoved(iface));
-    }
-
     /**
      * Translate the input v4 address to v6 clat address.
      */
diff --git a/service/src/com/android/server/connectivity/NetworkDiagnostics.java b/service/src/com/android/server/connectivity/NetworkDiagnostics.java
index e1e2585..3db37e5 100644
--- a/service/src/com/android/server/connectivity/NetworkDiagnostics.java
+++ b/service/src/com/android/server/connectivity/NetworkDiagnostics.java
@@ -340,8 +340,9 @@
     @TargetApi(Build.VERSION_CODES.S)
     private int getMtuForTarget(InetAddress target) {
         final int family = target instanceof Inet4Address ? AF_INET : AF_INET6;
+        FileDescriptor socket = null;
         try {
-            final FileDescriptor socket = Os.socket(family, SOCK_DGRAM, 0);
+            socket = Os.socket(family, SOCK_DGRAM, 0);
             mNetwork.bindSocket(socket);
             Os.connect(socket, target, 0);
             if (family == AF_INET) {
@@ -352,6 +353,8 @@
         } catch (ErrnoException | IOException e) {
             Log.e(TAG, "Can't get MTU for destination " + target, e);
             return -1;
+        } finally {
+            IoUtils.closeQuietly(socket);
         }
     }
 
diff --git a/service/src/com/android/server/connectivity/NetworkRanker.java b/service/src/com/android/server/connectivity/NetworkRanker.java
index d94c8dc..c473444 100644
--- a/service/src/com/android/server/connectivity/NetworkRanker.java
+++ b/service/src/com/android/server/connectivity/NetworkRanker.java
@@ -17,6 +17,8 @@
 package com.android.server.connectivity;
 
 import static android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_PRIORITIZE_BANDWIDTH;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_PRIORITIZE_LATENCY;
 import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
@@ -221,6 +223,19 @@
     }
 
     /**
+     * Returns whether the scorable has any of the PRIORITIZE_* capabilities.
+     *
+     * These capabilities code for customer slices, and a network that has one is a customer slice.
+     */
+    private boolean hasPrioritizedCapability(@NonNull final Scoreable nai) {
+        final NetworkCapabilities caps = nai.getCapsNoCopy();
+        final long anyPrioritizeCapability =
+                (1L << NET_CAPABILITY_PRIORITIZE_LATENCY)
+                | (1L << NET_CAPABILITY_PRIORITIZE_BANDWIDTH);
+        return 0 != (caps.getCapabilitiesInternal() & anyPrioritizeCapability);
+    }
+
+    /**
      * Get the best network among a list of candidates according to policy.
      * @param candidates the candidates
      * @param currentSatisfier the current satisfier, or null if none
@@ -324,6 +339,12 @@
         // change from the previous result. If there were, it's guaranteed candidates.size() > 0
         // because accepted.size() > 0 above.
 
+        // If any network is not a slice with prioritized bandwidth or latency, don't choose one
+        // that is.
+        partitionInto(candidates, nai -> !hasPrioritizedCapability(nai), accepted, rejected);
+        if (accepted.size() == 1) return accepted.get(0);
+        if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
+
         // If some of the networks have a better transport than others, keep only the ones with
         // the best transports.
         for (final int transport : PREFERRED_TRANSPORTS_ORDER) {
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index ee79ef2..621759e 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -48,7 +48,7 @@
       // "src_devicecommon/**/*.kt",
   ],
   sdk_version: "module_current",
-  min_sdk_version: "29",
+  min_sdk_version: "30",
   target_sdk_version: "30",
   apex_available: [
       "//apex_available:anyapex",
@@ -74,7 +74,10 @@
       "framework-configinfrastructure",
       "framework-connectivity.stubs.module_lib",
   ],
-  lint: { strict_updatability_linting: true },
+  lint: {
+      strict_updatability_linting: true,
+      error_checks: ["NewApi"],
+  },
 }
 
 java_defaults {
@@ -128,7 +131,7 @@
         "framework/com/android/net/module/util/HexDump.java",
     ],
     sdk_version: "module_current",
-    min_sdk_version: "29",
+    min_sdk_version: "30",
     visibility: [
         "//packages/modules/Connectivity:__subpackages__",
         "//packages/modules/NetworkStack:__subpackages__",
@@ -141,7 +144,10 @@
         "com.android.tethering",
         "//apex_available:platform",
     ],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+        error_checks: ["NewApi"],
+    },
 }
 
 java_library {
@@ -153,7 +159,7 @@
         "device/com/android/net/module/util/structs/*.java",
     ],
     sdk_version: "module_current",
-    min_sdk_version: "29",
+    min_sdk_version: "30",
     visibility: [
         "//packages/modules/Connectivity:__subpackages__",
         "//packages/modules/NetworkStack:__subpackages__",
@@ -169,7 +175,10 @@
         "com.android.tethering",
         "//apex_available:platform",
     ],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+        error_checks: ["NewApi"],
+    },
 }
 
 java_library {
@@ -178,7 +187,7 @@
         "device/com/android/net/module/util/netlink/*.java",
     ],
     sdk_version: "module_current",
-    min_sdk_version: "29",
+    min_sdk_version: "30",
     visibility: [
         "//packages/modules/Connectivity:__subpackages__",
         "//packages/modules/NetworkStack:__subpackages__",
@@ -194,7 +203,10 @@
         "com.android.tethering",
         "//apex_available:platform",
     ],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+        error_checks: ["NewApi"],
+    },
 }
 
 java_library {
@@ -204,7 +216,7 @@
         "device/com/android/net/module/util/ip/*.java",
     ],
     sdk_version: "module_current",
-    min_sdk_version: "29",
+    min_sdk_version: "30",
     visibility: [
         "//packages/modules/Connectivity:__subpackages__",
         "//packages/modules/NetworkStack:__subpackages__",
@@ -223,7 +235,10 @@
         "com.android.tethering",
         "//apex_available:platform",
     ],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+        error_checks: ["NewApi"],
+    },
 }
 
 java_library {
@@ -232,7 +247,7 @@
         ":net-utils-framework-common-srcs",
     ],
     sdk_version: "module_current",
-    min_sdk_version: "29",
+    min_sdk_version: "30",
     libs: [
         "androidx.annotation_annotation",
         "framework-annotations-lib",
@@ -258,7 +273,10 @@
         "//packages/modules/Wifi/framework/tests:__subpackages__",
         "//packages/apps/Settings",
     ],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+        error_checks: ["NewApi"],
+    },
     errorprone: {
         enabled: true,
         // Error-prone checking only warns of problems when building. To make the build fail with
@@ -301,7 +319,10 @@
         "//packages/modules/Bluetooth/android/app",
         "//packages/modules/Wifi/service:__subpackages__",
     ],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+        error_checks: ["NewApi"],
+    },
 }
 
 java_library {
@@ -310,7 +331,7 @@
         "device/com/android/net/module/util/async/*.java",
     ],
     sdk_version: "module_current",
-    min_sdk_version: "29",
+    min_sdk_version: "30",
     visibility: [
         "//packages/modules/Connectivity:__subpackages__",
     ],
@@ -323,7 +344,10 @@
         "com.android.tethering",
         "//apex_available:platform",
     ],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+        error_checks: ["NewApi"],
+    },
 }
 
 java_library {
@@ -332,7 +356,7 @@
         "device/com/android/net/module/util/wear/*.java",
     ],
     sdk_version: "module_current",
-    min_sdk_version: "29",
+    min_sdk_version: "30",
     visibility: [
         "//packages/modules/Connectivity:__subpackages__",
     ],
@@ -346,7 +370,10 @@
         "com.android.tethering",
         "//apex_available:platform",
     ],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+        error_checks: ["NewApi"],
+    },
 }
 
 // Limited set of utilities for use by service-connectivity-mdns-standalone-build-test, to make sure
diff --git a/staticlibs/client-libs/Android.bp b/staticlibs/client-libs/Android.bp
index c560045..c938dd6 100644
--- a/staticlibs/client-libs/Android.bp
+++ b/staticlibs/client-libs/Android.bp
@@ -6,7 +6,7 @@
     name: "netd-client",
     srcs: ["netd/**/*"],
     sdk_version: "system_current",
-    min_sdk_version: "29",
+    min_sdk_version: "30",
     apex_available: [
         "//apex_available:platform",
         "com.android.tethering",
diff --git a/staticlibs/client-libs/tests/unit/Android.bp b/staticlibs/client-libs/tests/unit/Android.bp
index 220a6c1..03e3e70 100644
--- a/staticlibs/client-libs/tests/unit/Android.bp
+++ b/staticlibs/client-libs/tests/unit/Android.bp
@@ -8,7 +8,7 @@
         "src/**/*.java",
         "src/**/*.kt",
     ],
-    min_sdk_version: "29",
+    min_sdk_version: "30",
     static_libs: [
         "androidx.test.rules",
         "mockito-target-extended-minus-junit4",
diff --git a/staticlibs/device/com/android/net/module/util/BpfUtils.java b/staticlibs/device/com/android/net/module/util/BpfUtils.java
index 94af11b..f1546c0 100644
--- a/staticlibs/device/com/android/net/module/util/BpfUtils.java
+++ b/staticlibs/device/com/android/net/module/util/BpfUtils.java
@@ -66,4 +66,6 @@
             throws IOException;
     private static native boolean native_detachSingleProgramFromCgroup(int type,
             String programPath, String cgroupPath) throws IOException;
+    private static native int native_getProgramIdFromCgroup(int type, String cgroupPath)
+            throws IOException;
 }
diff --git a/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
index e646f37..42f26f4 100644
--- a/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
+++ b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
@@ -40,6 +40,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.function.Supplier;
 
 /**
  * Utilities for modules to query {@link DeviceConfig} and flags.
@@ -63,6 +64,9 @@
     @VisibleForTesting
     public static final long DEFAULT_PACKAGE_VERSION = 1000;
 
+    private static final String CORE_NETWORKING_TRUNK_STABLE_NAMESPACE = "android_core_networking";
+    private static final String CORE_NETWORKING_TRUNK_STABLE_FLAG_PACKAGE = "com.android.net.flags";
+
     @VisibleForTesting
     public static void resetPackageVersionCacheForTest() {
         sPackageVersion = -1;
@@ -70,6 +74,9 @@
         sNetworkStackModuleVersion = -1;
     }
 
+    private static final int FORCE_ENABLE_FEATURE_FLAG_VALUE = 1;
+    private static final int FORCE_DISABLE_FEATURE_FLAG_VALUE = -1;
+
     private static volatile long sPackageVersion = -1;
     private static long getPackageVersion(@NonNull final Context context) {
         // sPackageVersion may be set by another thread just after this check, but querying the
@@ -161,34 +168,19 @@
      *
      * This is useful to ensure that if a module install is rolled back, flags are not left fully
      * rolled out on a version where they have not been well tested.
+     *
+     * If the feature is disabled by default and enabled by flag push, this method should be used.
+     * If the feature is enabled by default and disabled by flag push (kill switch),
+     * {@link #isNetworkStackFeatureNotChickenedOut(Context, String)} should be used.
+     *
      * @param context The global context information about an app environment.
      * @param name The name of the property to look up.
      * @return true if this feature is enabled, or false if disabled.
      */
     public static boolean isNetworkStackFeatureEnabled(@NonNull Context context,
             @NonNull String name) {
-        return isNetworkStackFeatureEnabled(context, name, false /* defaultEnabled */);
-    }
-
-    /**
-     * Check whether or not one specific experimental feature for a particular namespace from
-     * {@link DeviceConfig} is enabled by comparing module package version
-     * with current version of property. If this property version is valid, the corresponding
-     * experimental feature would be enabled, otherwise disabled.
-     *
-     * This is useful to ensure that if a module install is rolled back, flags are not left fully
-     * rolled out on a version where they have not been well tested.
-     * @param context The global context information about an app environment.
-     * @param name The name of the property to look up.
-     * @param defaultEnabled The value to return if the property does not exist or its value is
-     *                       null.
-     * @return true if this feature is enabled, or false if disabled.
-     */
-    public static boolean isNetworkStackFeatureEnabled(@NonNull Context context,
-            @NonNull String name, boolean defaultEnabled) {
-        final long packageVersion = getPackageVersion(context);
-        return isFeatureEnabled(context, packageVersion, NAMESPACE_CONNECTIVITY, name,
-                defaultEnabled);
+        return isFeatureEnabled(NAMESPACE_CONNECTIVITY, name, false /* defaultEnabled */,
+                () -> getPackageVersion(context));
     }
 
     /**
@@ -202,7 +194,7 @@
      *
      * If the feature is disabled by default and enabled by flag push, this method should be used.
      * If the feature is enabled by default and disabled by flag push (kill switch),
-     * {@link #isTetheringFeatureNotChickenedOut(String)} should be used.
+     * {@link #isTetheringFeatureNotChickenedOut(Context, String)} should be used.
      *
      * @param context The global context information about an app environment.
      * @param name The name of the property to look up.
@@ -210,17 +202,24 @@
      */
     public static boolean isTetheringFeatureEnabled(@NonNull Context context,
             @NonNull String name) {
-        final long packageVersion = getTetheringModuleVersion(context);
-        return isFeatureEnabled(context, packageVersion, NAMESPACE_TETHERING, name,
-                false /* defaultEnabled */);
+        return isFeatureEnabled(NAMESPACE_TETHERING, name, false /* defaultEnabled */,
+                () -> getTetheringModuleVersion(context));
     }
 
-    private static boolean isFeatureEnabled(@NonNull Context context, long packageVersion,
-            @NonNull String namespace, String name, boolean defaultEnabled) {
-        final int propertyVersion = getDeviceConfigPropertyInt(namespace, name,
-                0 /* default value */);
-        return (propertyVersion == 0 && defaultEnabled)
-                || (propertyVersion != 0 && packageVersion >= (long) propertyVersion);
+    private static boolean isFeatureEnabled(@NonNull String namespace,
+            String name, boolean defaultEnabled, Supplier<Long> packageVersionSupplier) {
+        final int flagValue = getDeviceConfigPropertyInt(namespace, name, 0 /* default value */);
+        switch (flagValue) {
+            case 0:
+                return defaultEnabled;
+            case FORCE_DISABLE_FEATURE_FLAG_VALUE:
+                return false;
+            case FORCE_ENABLE_FEATURE_FLAG_VALUE:
+                return true;
+            default:
+                final long packageVersion = packageVersionSupplier.get();
+                return packageVersion >= (long) flagValue;
+        }
     }
 
     // Guess the tethering module name based on the package prefix of the connectivity resources
@@ -331,42 +330,38 @@
     }
 
     /**
-     * Check whether one specific experimental feature in specific namespace from
-     * {@link DeviceConfig} is not disabled. Feature can be disabled by setting a non-zero
-     * value in the property. If the feature is enabled by default and disabled by flag push
-     * (kill switch), this method should be used. If the feature is disabled by default and
-     * enabled by flag push, {@link #isFeatureEnabled} should be used.
-     *
-     * @param namespace The namespace containing the property to look up.
-     * @param name The name of the property to look up.
-     * @return true if this feature is enabled, or false if disabled.
-     */
-    private static boolean isFeatureNotChickenedOut(String namespace, String name) {
-        final int propertyVersion = getDeviceConfigPropertyInt(namespace, name,
-                0 /* default value */);
-        return propertyVersion == 0;
-    }
-
-    /**
      * Check whether one specific experimental feature in Tethering module from {@link DeviceConfig}
      * is not disabled.
+     * If the feature is enabled by default and disabled by flag push (kill switch), this method
+     * should be used.
+     * If the feature is disabled by default and enabled by flag push,
+     * {@link #isTetheringFeatureEnabled(Context, String)} should be used.
      *
+     * @param context The global context information about an app environment.
      * @param name The name of the property in tethering module to look up.
      * @return true if this feature is enabled, or false if disabled.
      */
-    public static boolean isTetheringFeatureNotChickenedOut(String name) {
-        return isFeatureNotChickenedOut(NAMESPACE_TETHERING, name);
+    public static boolean isTetheringFeatureNotChickenedOut(@NonNull Context context, String name) {
+        return isFeatureEnabled(NAMESPACE_TETHERING, name, true /* defaultEnabled */,
+                () -> getTetheringModuleVersion(context));
     }
 
     /**
      * Check whether one specific experimental feature in NetworkStack module from
      * {@link DeviceConfig} is not disabled.
+     * If the feature is enabled by default and disabled by flag push (kill switch), this method
+     * should be used.
+     * If the feature is disabled by default and enabled by flag push,
+     * {@link #isNetworkStackFeatureEnabled(Context, String)} should be used.
      *
+     * @param context The global context information about an app environment.
      * @param name The name of the property in NetworkStack module to look up.
      * @return true if this feature is enabled, or false if disabled.
      */
-    public static boolean isNetworkStackFeatureNotChickenedOut(String name) {
-        return isFeatureNotChickenedOut(NAMESPACE_CONNECTIVITY, name);
+    public static boolean isNetworkStackFeatureNotChickenedOut(
+            @NonNull Context context, String name) {
+        return isFeatureEnabled(NAMESPACE_CONNECTIVITY, name, true /* defaultEnabled */,
+                () -> getPackageVersion(context));
     }
 
     /**
@@ -414,4 +409,31 @@
 
         return pkgs.get(0).activityInfo.applicationInfo.packageName;
     }
+
+    /**
+     * Check whether one specific trunk stable flag in android_core_networking namespace is enabled.
+     * This method reads trunk stable feature flag value from DeviceConfig directly since
+     * java_aconfig_library soong module is not available in the mainline branch.
+     * After the mainline branch support the aconfig soong module, this function must be removed and
+     * java_aconfig_library must be used instead to check if the feature is enabled.
+     *
+     * @param flagName The name of the trunk stable flag
+     * @return true if this feature is enabled, or false if disabled.
+     */
+    public static boolean isTrunkStableFeatureEnabled(final String flagName) {
+        return isTrunkStableFeatureEnabled(
+                CORE_NETWORKING_TRUNK_STABLE_NAMESPACE,
+                CORE_NETWORKING_TRUNK_STABLE_FLAG_PACKAGE,
+                flagName
+        );
+    }
+
+    private static boolean isTrunkStableFeatureEnabled(final String namespace,
+            final String packageName, final String flagName) {
+        return DeviceConfig.getBoolean(
+                namespace,
+                packageName + "." + flagName,
+                false /* defaultValue */
+        );
+    }
 }
diff --git a/staticlibs/device/com/android/net/module/util/SocketUtils.java b/staticlibs/device/com/android/net/module/util/SocketUtils.java
index 9878ea5..5e6a6c6 100644
--- a/staticlibs/device/com/android/net/module/util/SocketUtils.java
+++ b/staticlibs/device/com/android/net/module/util/SocketUtils.java
@@ -19,6 +19,8 @@
 import static android.net.util.SocketUtils.closeSocket;
 
 import android.annotation.NonNull;
+import android.annotation.RequiresApi;
+import android.os.Build;
 import android.system.NetlinkSocketAddress;
 
 import java.io.FileDescriptor;
@@ -39,7 +41,7 @@
     /**
      * Make a socket address to communicate with netlink.
      */
-    @NonNull
+    @NonNull @RequiresApi(Build.VERSION_CODES.S)
     public static SocketAddress makeNetlinkSocketAddress(int portId, int groupsMask) {
         return new NetlinkSocketAddress(portId, groupsMask);
     }
diff --git a/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java b/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java
index 59d655c..176b7bc 100644
--- a/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java
+++ b/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java
@@ -18,6 +18,8 @@
 
 import static com.android.net.module.util.NetworkStackConstants.DHCP6_OPTION_IAPREFIX;
 
+import android.util.Log;
+
 import com.android.net.module.util.Struct;
 import com.android.net.module.util.Struct.Field;
 import com.android.net.module.util.Struct.Type;
@@ -52,6 +54,7 @@
  * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  */
 public class IaPrefixOption extends Struct {
+    private static final String TAG = IaPrefixOption.class.getSimpleName();
     public static final int LENGTH = 25; // option length excluding IAprefix-options
 
     @Field(order = 0, type = Type.S16)
@@ -78,6 +81,33 @@
     }
 
     /**
+     * Check whether or not IA Prefix option in IA_PD option is valid per RFC8415#section-21.22.
+     */
+    public boolean isValid(int t2) {
+        if (preferred < 0 || valid < 0) {
+            Log.w(TAG, "IA_PD option with invalid lifetime, preferred lifetime " + preferred
+                    + ", valid lifetime " + valid);
+            return false;
+        }
+        if (preferred > valid) {
+            Log.w(TAG, "IA_PD option with preferred lifetime " + preferred
+                    + " greater than valid lifetime " + valid);
+            return false;
+        }
+        if (prefixLen > 64) {
+            Log.w(TAG, "IA_PD option with prefix length " + prefixLen
+                    + " longer than 64");
+            return false;
+        }
+        // Either preferred lifetime or t2 might be 0 which is valid, then ignore it.
+        if (preferred != 0 && t2 != 0 && preferred < t2) {
+            Log.w(TAG, "preferred lifetime " + preferred + " is smaller than T2 " + t2);
+            return false;
+        }
+        return true;
+    }
+
+    /**
      * Build an IA_PD prefix option with given specific parameters.
      */
     public static ByteBuffer build(final short length, final long preferred, final long valid,
diff --git a/staticlibs/lint-baseline.xml b/staticlibs/lint-baseline.xml
deleted file mode 100644
index d413b2a..0000000
--- a/staticlibs/lint-baseline.xml
+++ /dev/null
@@ -1,70 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 30 (current min is 29): `android.net.LinkProperties#getAddresses`"
-        errorLine1="        final Collection&lt;InetAddress&gt; leftAddresses = left.getAddresses();"
-        errorLine2="                                                           ~~~~~~~~~~~~">
-        <location
-            file="frameworks/libs/net/common/framework/com/android/net/module/util/LinkPropertiesUtils.java"
-            line="158"
-            column="60"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 30 (current min is 29): `android.net.LinkProperties#getAddresses`"
-        errorLine1="        final Collection&lt;InetAddress&gt; rightAddresses = right.getAddresses();"
-        errorLine2="                                                             ~~~~~~~~~~~~">
-        <location
-            file="frameworks/libs/net/common/framework/com/android/net/module/util/LinkPropertiesUtils.java"
-            line="159"
-            column="62"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 30 (current min is 29): `android.net.NetworkStats#addEntry`"
-        errorLine1="            stats = stats.addEntry(entry);"
-        errorLine2="                          ~~~~~~~~">
-        <location
-            file="frameworks/libs/net/common/framework/com/android/net/module/util/NetworkStatsUtils.java"
-            line="113"
-            column="27"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 30 (current min is 29): `new android.net.NetworkStats.Entry`"
-        errorLine1="        return new android.net.NetworkStats.Entry("
-        errorLine2="               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/libs/net/common/framework/com/android/net/module/util/NetworkStatsUtils.java"
-            line="120"
-            column="16"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 30 (current min is 29): `new android.net.NetworkStats`"
-        errorLine1="        android.net.NetworkStats stats = new android.net.NetworkStats(0L, 0);"
-        errorLine2="                                         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/libs/net/common/framework/com/android/net/module/util/NetworkStatsUtils.java"
-            line="108"
-            column="42"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 29): `new android.system.NetlinkSocketAddress`"
-        errorLine1="        return new NetlinkSocketAddress(portId, groupsMask);"
-        errorLine2="               ~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="frameworks/libs/net/common/device/com/android/net/module/util/SocketUtils.java"
-            line="44"
-            column="16"/>
-    </issue>
-
-</issues>
\ No newline at end of file
diff --git a/staticlibs/native/bpf_headers/BpfMapTest.cpp b/staticlibs/native/bpf_headers/BpfMapTest.cpp
index 10afe86..862114d 100644
--- a/staticlibs/native/bpf_headers/BpfMapTest.cpp
+++ b/staticlibs/native/bpf_headers/BpfMapTest.cpp
@@ -107,12 +107,14 @@
     BpfMap<uint32_t, uint32_t> testMap1;
     checkMapInvalid(testMap1);
 
-    BpfMap<uint32_t, uint32_t> testMap2(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, BPF_F_NO_PREALLOC);
+    BpfMap<uint32_t, uint32_t> testMap2;
+    ASSERT_RESULT_OK(testMap2.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, BPF_F_NO_PREALLOC));
     checkMapValid(testMap2);
 }
 
 TEST_F(BpfMapTest, basicHelpers) {
-    BpfMap<uint32_t, uint32_t> testMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, BPF_F_NO_PREALLOC);
+    BpfMap<uint32_t, uint32_t> testMap;
+    ASSERT_RESULT_OK(testMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, BPF_F_NO_PREALLOC));
     uint32_t key = TEST_KEY1;
     uint32_t value_write = TEST_VALUE1;
     writeToMapAndCheck(testMap, key, value_write);
@@ -126,7 +128,8 @@
 }
 
 TEST_F(BpfMapTest, reset) {
-    BpfMap<uint32_t, uint32_t> testMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, BPF_F_NO_PREALLOC);
+    BpfMap<uint32_t, uint32_t> testMap;
+    ASSERT_RESULT_OK(testMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, BPF_F_NO_PREALLOC));
     uint32_t key = TEST_KEY1;
     uint32_t value_write = TEST_VALUE1;
     writeToMapAndCheck(testMap, key, value_write);
@@ -138,7 +141,8 @@
 }
 
 TEST_F(BpfMapTest, moveConstructor) {
-    BpfMap<uint32_t, uint32_t> testMap1(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, BPF_F_NO_PREALLOC);
+    BpfMap<uint32_t, uint32_t> testMap1;
+    ASSERT_RESULT_OK(testMap1.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, BPF_F_NO_PREALLOC));
     BpfMap<uint32_t, uint32_t> testMap2;
     testMap2 = std::move(testMap1);
     uint32_t key = TEST_KEY1;
@@ -149,7 +153,8 @@
 
 TEST_F(BpfMapTest, SetUpMap) {
     EXPECT_NE(0, access(PINNED_MAP_PATH, R_OK));
-    BpfMap<uint32_t, uint32_t> testMap1(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, BPF_F_NO_PREALLOC);
+    BpfMap<uint32_t, uint32_t> testMap1;
+    ASSERT_RESULT_OK(testMap1.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, BPF_F_NO_PREALLOC));
     ASSERT_EQ(0, bpfFdPin(testMap1.getMap(), PINNED_MAP_PATH));
     EXPECT_EQ(0, access(PINNED_MAP_PATH, R_OK));
     checkMapValid(testMap1);
@@ -164,7 +169,8 @@
 }
 
 TEST_F(BpfMapTest, iterate) {
-    BpfMap<uint32_t, uint32_t> testMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, BPF_F_NO_PREALLOC);
+    BpfMap<uint32_t, uint32_t> testMap;
+    ASSERT_RESULT_OK(testMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, BPF_F_NO_PREALLOC));
     populateMap(TEST_MAP_SIZE, testMap);
     int totalCount = 0;
     int totalSum = 0;
@@ -182,7 +188,8 @@
 }
 
 TEST_F(BpfMapTest, iterateWithValue) {
-    BpfMap<uint32_t, uint32_t> testMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, BPF_F_NO_PREALLOC);
+    BpfMap<uint32_t, uint32_t> testMap;
+    ASSERT_RESULT_OK(testMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, BPF_F_NO_PREALLOC));
     populateMap(TEST_MAP_SIZE, testMap);
     int totalCount = 0;
     int totalSum = 0;
@@ -202,7 +209,8 @@
 }
 
 TEST_F(BpfMapTest, mapIsEmpty) {
-    BpfMap<uint32_t, uint32_t> testMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, BPF_F_NO_PREALLOC);
+    BpfMap<uint32_t, uint32_t> testMap;
+    ASSERT_RESULT_OK(testMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, BPF_F_NO_PREALLOC));
     expectMapEmpty(testMap);
     uint32_t key = TEST_KEY1;
     uint32_t value_write = TEST_VALUE1;
@@ -232,7 +240,8 @@
 }
 
 TEST_F(BpfMapTest, mapClear) {
-    BpfMap<uint32_t, uint32_t> testMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, BPF_F_NO_PREALLOC);
+    BpfMap<uint32_t, uint32_t> testMap;
+    ASSERT_RESULT_OK(testMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE));
     populateMap(TEST_MAP_SIZE, testMap);
     Result<bool> isEmpty = testMap.isEmpty();
     ASSERT_RESULT_OK(isEmpty);
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h b/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h
index dd0804c..81be37d 100644
--- a/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h
@@ -22,9 +22,15 @@
 // Reject the packet
 #define BPF_REJECT BPF_STMT(BPF_RET | BPF_K, 0)
 
+// Note arguments to BPF_JUMP(opcode, operand, true_offset, false_offset)
+
+// If not equal, jump over count instructions
+#define BPF_JUMP_IF_NOT_EQUAL(v, count) \
+	BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (v), 0, (count))
+
 // *TWO* instructions: compare and if not equal jump over the accept statement
 #define BPF2_ACCEPT_IF_EQUAL(v) \
-	BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (v), 0, 1), \
+	BPF_JUMP_IF_NOT_EQUAL((v), 1), \
 	BPF_ACCEPT
 
 // *TWO* instructions: compare and if equal jump over the reject statement
@@ -32,8 +38,24 @@
 	BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (v), 1, 0), \
 	BPF_REJECT
 
+// *TWO* instructions: compare and if greater or equal jump over the reject statement
+#define BPF2_REJECT_IF_LESS_THAN(v) \
+	BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, (v), 1, 0), \
+	BPF_REJECT
+
+// *TWO* instructions: compare and if *NOT* greater jump over the reject statement
+#define BPF2_REJECT_IF_GREATER_THAN(v) \
+	BPF_JUMP(BPF_JMP | BPF_JGT | BPF_K, (v), 0, 1), \
+	BPF_REJECT
+
+// *THREE* instructions: compare and if *NOT* in range [lo, hi], jump over the reject statement
+#define BPF3_REJECT_IF_NOT_IN_RANGE(lo, hi) \
+	BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, (lo), 0, 1), \
+	BPF_JUMP(BPF_JMP | BPF_JGT | BPF_K, (hi), 0, 1), \
+	BPF_REJECT
+
 // *TWO* instructions: compare and if none of the bits are set jump over the reject statement
-#define BPF2_REJECT_IF_ANY_BITS_SET(v) \
+#define BPF2_REJECT_IF_ANY_MASKED_BITS_SET(v) \
 	BPF_JUMP(BPF_JMP | BPF_JSET | BPF_K, (v), 0, 1), \
 	BPF_REJECT
 
@@ -108,3 +130,55 @@
 	  _Static_assert(field_sizeof(struct ipv6hdr, field) == 4, "field of wrong size"); \
 	  offsetof(ipv6hdr, field); \
 	}))
+
+// Load the length of the IPv4 header into X index register.
+// ie. X := 4 * IPv4.IHL, where IPv4.IHL is the bottom nibble
+// of the first byte of the IPv4 (aka network layer) header.
+#define BPF_LOADX_NET_RELATIVE_IPV4_HLEN \
+    BPF_STMT(BPF_LDX | BPF_B | BPF_MSH, (__u32)SKF_NET_OFF)
+
+// Blindly assumes no IPv6 extension headers, just does X := 40
+// You may later adjust this as you parse through IPv6 ext hdrs.
+#define BPF_LOADX_CONSTANT_IPV6_HLEN \
+    BPF_STMT(BPF_LDX | BPF_W | BPF_IMM, sizeof(struct ipv6hdr))
+
+// NOTE: all the following require X to be setup correctly (v4: 20+, v6: 40+)
+
+// 8-bit load from L4 (TCP/UDP/...) header
+#define BPF_LOAD_NETX_RELATIVE_L4_U8(ofs) \
+    BPF_STMT(BPF_LD | BPF_B | BPF_IND, (__u32)SKF_NET_OFF + (ofs))
+
+// Big/Network Endian 16-bit load from L4 (TCP/UDP/...) header
+#define BPF_LOAD_NETX_RELATIVE_L4_BE16(ofs) \
+    BPF_STMT(BPF_LD | BPF_H | BPF_IND, (__u32)SKF_NET_OFF + (ofs))
+
+// Big/Network Endian 32-bit load from L4 (TCP/UDP/...) header
+#define BPF_LOAD_NETX_RELATIVE_L4_BE32(ofs) \
+    BPF_STMT(BPF_LD | BPF_W | BPF_IND, (__u32)SKF_NET_OFF + (ofs))
+
+// Both ICMPv4 and ICMPv6 start with u8 type, u8 code
+#define BPF_LOAD_NETX_RELATIVE_ICMP_TYPE BPF_LOAD_NETX_RELATIVE_L4_U8(0)
+#define BPF_LOAD_NETX_RELATIVE_ICMP_CODE BPF_LOAD_NETX_RELATIVE_L4_U8(1)
+
+// IPv6 extension headers (HOPOPTS, DSTOPS, FRAG) begin with a u8 nexthdr
+#define BPF_LOAD_NETX_RELATIVE_V6EXTHDR_NEXTHDR BPF_LOAD_NETX_RELATIVE_L4_U8(0)
+
+// IPv6 fragment header is always exactly 8 bytes long
+#define BPF_LOAD_CONSTANT_V6FRAGHDR_LEN \
+    BPF_STMT(BPF_LD | BPF_IMM, 8)
+
+// HOPOPTS/DSTOPS follow up with 'u8 len', counting 8 byte units, (0->8, 1->16)
+// *THREE* instructions
+#define BPF3_LOAD_NETX_RELATIVE_V6EXTHDR_LEN \
+    BPF_LOAD_NETX_RELATIVE_L4_U8(1), \
+    BPF_STMT(BPF_ALU | BPF_ADD | BPF_K, 1), \
+    BPF_STMT(BPF_ALU | BPF_LSH | BPF_K, 3)
+
+// *TWO* instructions: A += X; X := A
+#define BPF2_ADD_A_TO_X \
+    BPF_STMT(BPF_ALU | BPF_ADD | BPF_X, 0), \
+    BPF_STMT(BPF_MISC | BPF_TAX, 0)
+
+// UDP/UDPLITE/TCP/SCTP/DCCP all start with be16 srcport, dstport
+#define BPF_LOAD_NETX_RELATIVE_SRC_PORT BPF_LOAD_NETX_RELATIVE_L4_BE16(0)
+#define BPF_LOAD_NETX_RELATIVE_DST_PORT BPF_LOAD_NETX_RELATIVE_L4_BE16(2)
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfMap.h b/staticlibs/native/bpf_headers/include/bpf/BpfMap.h
index 847083e..5d7eb0d 100644
--- a/staticlibs/native/bpf_headers/include/bpf/BpfMap.h
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfMap.h
@@ -18,10 +18,10 @@
 
 #include <linux/bpf.h>
 
+#include <android/log.h>
 #include <android-base/result.h>
 #include <android-base/stringprintf.h>
 #include <android-base/unique_fd.h>
-#include <utils/Log.h>
 
 #include "BpfSyscallWrappers.h"
 #include "bpf/BpfUtils.h"
@@ -48,41 +48,32 @@
 // is not safe to iterate over a map while another thread or process is deleting
 // from it. In this case the iteration can return duplicate entries.
 template <class Key, class Value>
-class BpfMap {
+class BpfMapRO {
   public:
-    BpfMap<Key, Value>() {};
+    BpfMapRO<Key, Value>() {};
 
     // explicitly force no copy constructor, since it would need to dup the fd
     // (later on, for testing, we still make available a copy assignment operator)
-    BpfMap<Key, Value>(const BpfMap<Key, Value>&) = delete;
+    BpfMapRO<Key, Value>(const BpfMapRO<Key, Value>&) = delete;
 
-  private:
-    void abortOnKeyOrValueSizeMismatch() {
+  protected:
+    void abortOnMismatch(bool writable) const {
         if (!mMapFd.ok()) abort();
         if (isAtLeastKernelVersion(4, 14, 0)) {
+            int flags = bpfGetFdMapFlags(mMapFd);
+            if (flags < 0) abort();
+            if (flags & BPF_F_WRONLY) abort();
+            if (writable && (flags & BPF_F_RDONLY)) abort();
             if (bpfGetFdKeySize(mMapFd) != sizeof(Key)) abort();
             if (bpfGetFdValueSize(mMapFd) != sizeof(Value)) abort();
         }
     }
 
-  protected:
-    // flag must be within BPF_OBJ_FLAG_MASK, ie. 0, BPF_F_RDONLY, BPF_F_WRONLY
-    BpfMap<Key, Value>(const char* pathname, uint32_t flags) {
-        mMapFd.reset(mapRetrieve(pathname, flags));
-        abortOnKeyOrValueSizeMismatch();
-    }
-
   public:
-    explicit BpfMap<Key, Value>(const char* pathname) : BpfMap<Key, Value>(pathname, 0) {}
-
-#ifdef BPF_MAP_MAKE_VISIBLE_FOR_TESTING
-    // All bpf maps should be created by the bpfloader, so this constructor
-    // is reserved for tests
-    BpfMap<Key, Value>(bpf_map_type map_type, uint32_t max_entries, uint32_t map_flags = 0) {
-        mMapFd.reset(createMap(map_type, sizeof(Key), sizeof(Value), max_entries, map_flags));
-        if (!mMapFd.ok()) abort();
+    explicit BpfMapRO<Key, Value>(const char* pathname) {
+        mMapFd.reset(mapRetrieveRO(pathname));
+        abortOnMismatch(/* writable */ false);
     }
-#endif
 
     Result<Key> getFirstKey() const {
         Key firstKey;
@@ -100,13 +91,6 @@
         return nextKey;
     }
 
-    Result<void> writeValue(const Key& key, const Value& value, uint64_t flags) {
-        if (writeToMapEntry(mMapFd, &key, &value, flags)) {
-            return ErrnoErrorf("Write to map {} failed", mMapFd.get());
-        }
-        return {};
-    }
-
     Result<Value> readValue(const Key key) const {
         Value value;
         if (findMapEntry(mMapFd, &key, &value)) {
@@ -115,6 +99,155 @@
         return value;
     }
 
+  protected:
+    [[clang::reinitializes]] Result<void> init(const char* path, int fd, bool writable) {
+        mMapFd.reset(fd);
+        if (!mMapFd.ok()) {
+            return ErrnoErrorf("Pinned map not accessible or does not exist: ({})", path);
+        }
+        // Normally we should return an error here instead of calling abort,
+        // but this cannot happen at runtime without a massive code bug (K/V type mismatch)
+        // and as such it's better to just blow the system up and let the developer fix it.
+        // Crashes are much more likely to be noticed than logs and missing functionality.
+        abortOnMismatch(writable);
+        return {};
+    }
+
+  public:
+    // Function that tries to get map from a pinned path.
+    [[clang::reinitializes]] Result<void> init(const char* path) {
+        return init(path, mapRetrieveRO(path), /* writable */ false);
+    }
+
+    // Iterate through the map and handle each key retrieved based on the filter
+    // without modification of map content.
+    Result<void> iterate(
+            const function<Result<void>(const Key& key,
+                                        const BpfMapRO<Key, Value>& map)>& filter) const;
+
+    // Iterate through the map and get each <key, value> pair, handle each <key,
+    // value> pair based on the filter without modification of map content.
+    Result<void> iterateWithValue(
+            const function<Result<void>(const Key& key, const Value& value,
+                                        const BpfMapRO<Key, Value>& map)>& filter) const;
+
+#ifdef BPF_MAP_MAKE_VISIBLE_FOR_TESTING
+    const unique_fd& getMap() const { return mMapFd; };
+
+    // Copy assignment operator - due to need for fd duping, should not be used in non-test code.
+    BpfMapRO<Key, Value>& operator=(const BpfMapRO<Key, Value>& other) {
+        if (this != &other) mMapFd.reset(fcntl(other.mMapFd.get(), F_DUPFD_CLOEXEC, 0));
+        return *this;
+    }
+#else
+    BpfMapRO<Key, Value>& operator=(const BpfMapRO<Key, Value>&) = delete;
+#endif
+
+    // Move assignment operator
+    BpfMapRO<Key, Value>& operator=(BpfMapRO<Key, Value>&& other) noexcept {
+        if (this != &other) {
+            mMapFd = std::move(other.mMapFd);
+            other.reset();
+        }
+        return *this;
+    }
+
+#ifdef BPF_MAP_MAKE_VISIBLE_FOR_TESTING
+    // Note that unique_fd.reset() carefully saves and restores the errno,
+    // and BpfMap.reset() won't touch the errno if passed in fd is negative either,
+    // hence you can do something like BpfMap.reset(systemcall()) and then
+    // check BpfMap.isValid() and look at errno and see why systemcall() failed.
+    [[clang::reinitializes]] void reset(int fd) {
+        mMapFd.reset(fd);
+        if (mMapFd.ok()) abortOnMismatch(/* writable */ false);  // false isn't ideal
+    }
+
+    // unique_fd has an implicit int conversion defined, which combined with the above
+    // reset(int) would result in double ownership of the fd, hence we either need a custom
+    // implementation of reset(unique_fd), or to delete it and thus cause compile failures
+    // to catch this and prevent it.
+    void reset(unique_fd fd) = delete;
+#endif
+
+    [[clang::reinitializes]] void reset() {
+        mMapFd.reset();
+    }
+
+    bool isValid() const { return mMapFd.ok(); }
+
+    Result<bool> isEmpty() const {
+        auto key = getFirstKey();
+        if (key.ok()) return false;
+        if (key.error().code() == ENOENT) return true;
+        return key.error();
+    }
+
+  protected:
+    unique_fd mMapFd;
+};
+
+template <class Key, class Value>
+Result<void> BpfMapRO<Key, Value>::iterate(
+        const function<Result<void>(const Key& key,
+                                    const BpfMapRO<Key, Value>& map)>& filter) const {
+    Result<Key> curKey = getFirstKey();
+    while (curKey.ok()) {
+        const Result<Key>& nextKey = getNextKey(curKey.value());
+        Result<void> status = filter(curKey.value(), *this);
+        if (!status.ok()) return status;
+        curKey = nextKey;
+    }
+    if (curKey.error().code() == ENOENT) return {};
+    return curKey.error();
+}
+
+template <class Key, class Value>
+Result<void> BpfMapRO<Key, Value>::iterateWithValue(
+        const function<Result<void>(const Key& key, const Value& value,
+                                    const BpfMapRO<Key, Value>& map)>& filter) const {
+    Result<Key> curKey = getFirstKey();
+    while (curKey.ok()) {
+        const Result<Key>& nextKey = getNextKey(curKey.value());
+        Result<Value> curValue = readValue(curKey.value());
+        if (!curValue.ok()) return curValue.error();
+        Result<void> status = filter(curKey.value(), curValue.value(), *this);
+        if (!status.ok()) return status;
+        curKey = nextKey;
+    }
+    if (curKey.error().code() == ENOENT) return {};
+    return curKey.error();
+}
+
+template <class Key, class Value>
+class BpfMap : public BpfMapRO<Key, Value> {
+  protected:
+    using BpfMapRO<Key, Value>::mMapFd;
+    using BpfMapRO<Key, Value>::abortOnMismatch;
+
+  public:
+    using BpfMapRO<Key, Value>::getFirstKey;
+    using BpfMapRO<Key, Value>::getNextKey;
+    using BpfMapRO<Key, Value>::readValue;
+
+    BpfMap<Key, Value>() {};
+
+    explicit BpfMap<Key, Value>(const char* pathname) {
+        mMapFd.reset(mapRetrieveRW(pathname));
+        abortOnMismatch(/* writable */ true);
+    }
+
+    // Function that tries to get map from a pinned path.
+    [[clang::reinitializes]] Result<void> init(const char* path) {
+        return BpfMapRO<Key,Value>::init(path, mapRetrieveRW(path), /* writable */ true);
+    }
+
+    Result<void> writeValue(const Key& key, const Value& value, uint64_t flags) {
+        if (writeToMapEntry(mMapFd, &key, &value, flags)) {
+            return ErrnoErrorf("Write to map {} failed", mMapFd.get());
+        }
+        return {};
+    }
+
     Result<void> deleteValue(const Key& key) {
         if (deleteMapEntry(mMapFd, &key)) {
             return ErrnoErrorf("Delete entry from map {} failed", mMapFd.get());
@@ -122,37 +255,33 @@
         return {};
     }
 
-  protected:
-    [[clang::reinitializes]] Result<void> init(const char* path, int fd) {
-        mMapFd.reset(fd);
-        if (!mMapFd.ok()) {
-            return ErrnoErrorf("Pinned map not accessible or does not exist: ({})", path);
+    Result<void> clear() {
+        while (true) {
+            auto key = getFirstKey();
+            if (!key.ok()) {
+                if (key.error().code() == ENOENT) return {};  // empty: success
+                return key.error();                           // Anything else is an error
+            }
+            auto res = deleteValue(key.value());
+            if (!res.ok()) {
+                // Someone else could have deleted the key, so ignore ENOENT
+                if (res.error().code() == ENOENT) continue;
+                ALOGE("Failed to delete data %s", strerror(res.error().code()));
+                return res.error();
+            }
         }
-        // Normally we should return an error here instead of calling abort,
-        // but this cannot happen at runtime without a massive code bug (K/V type mismatch)
-        // and as such it's better to just blow the system up and let the developer fix it.
-        // Crashes are much more likely to be noticed than logs and missing functionality.
-        abortOnKeyOrValueSizeMismatch();
-        return {};
     }
 
-  public:
-    // Function that tries to get map from a pinned path.
-    [[clang::reinitializes]] Result<void> init(const char* path) {
-        return init(path, mapRetrieveRW(path));
-    }
-
-
 #ifdef BPF_MAP_MAKE_VISIBLE_FOR_TESTING
-    // due to Android SELinux limitations which prevent map creation by anyone besides the bpfloader
-    // this should only ever be used by test code, it is equivalent to:
-    //   .reset(createMap(type, keysize, valuesize, max_entries, map_flags)
-    // TODO: derive map_flags from BpfMap vs BpfMapRO
     [[clang::reinitializes]] Result<void> resetMap(bpf_map_type map_type,
-                                                         uint32_t max_entries,
-                                                         uint32_t map_flags = 0) {
-        mMapFd.reset(createMap(map_type, sizeof(Key), sizeof(Value), max_entries, map_flags));
+                                                   uint32_t max_entries,
+                                                   uint32_t map_flags = 0) {
+        if (map_flags & BPF_F_WRONLY) abort();
+        if (map_flags & BPF_F_RDONLY) abort();
+        mMapFd.reset(createMap(map_type, sizeof(Key), sizeof(Value), max_entries,
+                               map_flags));
         if (!mMapFd.ok()) return ErrnoErrorf("Unable to create map.");
+        abortOnMismatch(/* writable */ true);
         return {};
     }
 #endif
@@ -180,72 +309,6 @@
             const function<Result<void>(const Key& key, const Value& value,
                                         BpfMap<Key, Value>& map)>& filter);
 
-#ifdef BPF_MAP_MAKE_VISIBLE_FOR_TESTING
-    const unique_fd& getMap() const { return mMapFd; };
-
-    // Copy assignment operator - due to need for fd duping, should not be used in non-test code.
-    BpfMap<Key, Value>& operator=(const BpfMap<Key, Value>& other) {
-        if (this != &other) mMapFd.reset(fcntl(other.mMapFd.get(), F_DUPFD_CLOEXEC, 0));
-        return *this;
-    }
-#else
-    BpfMap<Key, Value>& operator=(const BpfMap<Key, Value>&) = delete;
-#endif
-
-    // Move assignment operator
-    BpfMap<Key, Value>& operator=(BpfMap<Key, Value>&& other) noexcept {
-        if (this != &other) {
-            mMapFd = std::move(other.mMapFd);
-            other.reset();
-        }
-        return *this;
-    }
-
-    void reset(unique_fd fd) = delete;
-
-#ifdef BPF_MAP_MAKE_VISIBLE_FOR_TESTING
-    // Note that unique_fd.reset() carefully saves and restores the errno,
-    // and BpfMap.reset() won't touch the errno if passed in fd is negative either,
-    // hence you can do something like BpfMap.reset(systemcall()) and then
-    // check BpfMap.isValid() and look at errno and see why systemcall() failed.
-    [[clang::reinitializes]] void reset(int fd) {
-        mMapFd.reset(fd);
-        if (mMapFd.ok()) abortOnKeyOrValueSizeMismatch();
-    }
-#endif
-
-    [[clang::reinitializes]] void reset() {
-        mMapFd.reset();
-    }
-
-    bool isValid() const { return mMapFd.ok(); }
-
-    Result<void> clear() {
-        while (true) {
-            auto key = getFirstKey();
-            if (!key.ok()) {
-                if (key.error().code() == ENOENT) return {};  // empty: success
-                return key.error();                           // Anything else is an error
-            }
-            auto res = deleteValue(key.value());
-            if (!res.ok()) {
-                // Someone else could have deleted the key, so ignore ENOENT
-                if (res.error().code() == ENOENT) continue;
-                ALOGE("Failed to delete data %s", strerror(res.error().code()));
-                return res.error();
-            }
-        }
-    }
-
-    Result<bool> isEmpty() const {
-        auto key = getFirstKey();
-        if (key.ok()) return false;
-        if (key.error().code() == ENOENT) return true;
-        return key.error();
-    }
-
-  private:
-    unique_fd mMapFd;
 };
 
 template <class Key, class Value>
@@ -312,19 +375,5 @@
     return curKey.error();
 }
 
-template <class Key, class Value>
-class BpfMapRO : public BpfMap<Key, Value> {
-  public:
-    BpfMapRO<Key, Value>() {};
-
-    explicit BpfMapRO<Key, Value>(const char* pathname)
-        : BpfMap<Key, Value>(pathname, BPF_F_RDONLY) {}
-
-    // Function that tries to get map from a pinned path.
-    [[clang::reinitializes]] Result<void> init(const char* path) {
-        return BpfMap<Key,Value>::init(path, mapRetrieveRO(path));
-    }
-};
-
 }  // namespace bpf
 }  // namespace android
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
index 67ac0e4..baff09b 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
@@ -105,9 +105,19 @@
  * implemented in the kernel sources.
  */
 
-#define KVER_NONE 0
-#define KVER(a, b, c) (((a) << 24) + ((b) << 16) + (c))
-#define KVER_INF 0xFFFFFFFFu
+struct kver_uint { unsigned int kver; };
+#define KVER_(v) ((struct kver_uint){ .kver = (v) })
+#define KVER(a, b, c) KVER_(((a) << 24) + ((b) << 16) + (c))
+#define KVER_NONE KVER_(0)
+#define KVER_4_14 KVER(4, 14, 0)
+#define KVER_4_19 KVER(4, 19, 0)
+#define KVER_5_4 KVER(5, 4, 0)
+#define KVER_5_8 KVER(5, 8, 0)
+#define KVER_5_9 KVER(5, 9, 0)
+#define KVER_5_15 KVER(5, 15, 0)
+#define KVER_INF KVER_(0xFFFFFFFFu)
+
+#define KVER_IS_AT_LEAST(kver, a, b, c) ((kver).kver >= KVER(a, b, c).kver)
 
 /*
  * BPFFS (ie. /sys/fs/bpf) labelling is as follows:
@@ -188,10 +198,12 @@
         __attribute__ ((section(".maps." #name), used)) \
                 ____btf_map_##name = { }
 
-#define BPF_ASSERT_LOADER_VERSION(min_loader, ignore_eng, ignore_user, ignore_userdebug)  \
-    _Static_assert(                                                                       \
-        (min_loader) >= BPFLOADER_IGNORED_ON_VERSION ||                                   \
-            !((ignore_eng) || (ignore_user) || (ignore_userdebug)),                       \
+#define BPF_ASSERT_LOADER_VERSION(min_loader, ignore_eng, ignore_user, ignore_userdebug) \
+    _Static_assert(                                                                      \
+        (min_loader) >= BPFLOADER_IGNORED_ON_VERSION ||                                  \
+            !((ignore_eng).ignore_on_eng ||                                              \
+              (ignore_user).ignore_on_user ||                                            \
+              (ignore_userdebug).ignore_on_userdebug),                                   \
         "bpfloader min version must be >= 0.33 in order to use ignored_on");
 
 #define DEFINE_BPF_MAP_BASE(the_map, TYPE, keysize, valuesize, num_entries, \
@@ -209,14 +221,14 @@
         .mode = (md),                                                       \
         .bpfloader_min_ver = (minloader),                                   \
         .bpfloader_max_ver = (maxloader),                                   \
-        .min_kver = (minkver),                                              \
-        .max_kver = (maxkver),                                              \
+        .min_kver = (minkver).kver,                                         \
+        .max_kver = (maxkver).kver,                                         \
         .selinux_context = (selinux),                                       \
         .pin_subdir = (pindir),                                             \
-        .shared = (share),                                                  \
-        .ignore_on_eng = (ignore_eng),                                      \
-        .ignore_on_user = (ignore_user),                                    \
-        .ignore_on_userdebug = (ignore_userdebug),                          \
+        .shared = (share).shared,                                           \
+        .ignore_on_eng = (ignore_eng).ignore_on_eng,                        \
+        .ignore_on_user = (ignore_user).ignore_on_user,                     \
+        .ignore_on_userdebug = (ignore_userdebug).ignore_on_userdebug,      \
     };                                                                      \
     BPF_ASSERT_LOADER_VERSION(minloader, ignore_eng, ignore_user, ignore_userdebug);
 
@@ -230,7 +242,7 @@
                                selinux, pindir, share, min_loader, max_loader, \
                                ignore_eng, ignore_user, ignore_userdebug)      \
     DEFINE_BPF_MAP_BASE(the_map, RINGBUF, 0, 0, size_bytes, usr, grp, md,      \
-                        selinux, pindir, share, KVER(5, 8, 0), KVER_INF,       \
+                        selinux, pindir, share, KVER_5_8, KVER_INF,            \
                         min_loader, max_loader, ignore_eng, ignore_user,       \
                         ignore_userdebug);                                     \
                                                                                \
@@ -312,11 +324,11 @@
 #error "Bpf Map UID must be left at default of AID_ROOT for BpfLoader prior to v0.28"
 #endif
 
-#define DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md)   \
-    DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md,       \
-                       DEFAULT_BPF_MAP_SELINUX_CONTEXT, DEFAULT_BPF_MAP_PIN_SUBDIR, false, \
-                       BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, /*ignore_on_eng*/false,       \
-                       /*ignore_on_user*/false, /*ignore_on_userdebug*/false)
+#define DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md)     \
+    DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md,         \
+                       DEFAULT_BPF_MAP_SELINUX_CONTEXT, DEFAULT_BPF_MAP_PIN_SUBDIR, PRIVATE, \
+                       BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, LOAD_ON_ENG,                    \
+                       LOAD_ON_USER, LOAD_ON_USERDEBUG)
 
 #define DEFINE_BPF_MAP(the_map, TYPE, KeyType, ValueType, num_entries) \
     DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
@@ -362,16 +374,16 @@
     const struct bpf_prog_def SECTION("progs") the_prog##_def = {                        \
         .uid = (prog_uid),                                                               \
         .gid = (prog_gid),                                                               \
-        .min_kver = (min_kv),                                                            \
-        .max_kver = (max_kv),                                                            \
-        .optional = (opt),                                                               \
+        .min_kver = (min_kv).kver,                                                       \
+        .max_kver = (max_kv).kver,                                                       \
+        .optional = (opt).optional,                                                      \
         .bpfloader_min_ver = (min_loader),                                               \
         .bpfloader_max_ver = (max_loader),                                               \
         .selinux_context = (selinux),                                                    \
         .pin_subdir = (pindir),                                                          \
-        .ignore_on_eng = (ignore_eng),                                                   \
-        .ignore_on_user = (ignore_user),                                                 \
-        .ignore_on_userdebug = (ignore_userdebug),                                       \
+        .ignore_on_eng = (ignore_eng).ignore_on_eng,                                     \
+        .ignore_on_user = (ignore_user).ignore_on_user,                                  \
+        .ignore_on_userdebug = (ignore_userdebug).ignore_on_userdebug,                   \
     };                                                                                   \
     SECTION(SECTION_NAME)                                                                \
     int the_prog
@@ -389,7 +401,7 @@
     DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv,                \
                         BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, opt,                                 \
                         DEFAULT_BPF_PROG_SELINUX_CONTEXT, DEFAULT_BPF_PROG_PIN_SUBDIR,             \
-                        false, false, false)
+                        LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
 
 // Programs (here used in the sense of functions/sections) marked optional are allowed to fail
 // to load (for example due to missing kernel patches).
@@ -405,21 +417,24 @@
 // programs requiring a kernel version >= min_kv && < max_kv
 #define DEFINE_BPF_PROG_KVER_RANGE(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv) \
     DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, \
-                                   false)
+                                   MANDATORY)
 #define DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, \
                                             max_kv)                                             \
-    DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, true)
+    DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, \
+                                   OPTIONAL)
 
 // programs requiring a kernel version >= min_kv
 #define DEFINE_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv)                 \
     DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, KVER_INF, \
-                                   false)
+                                   MANDATORY)
 #define DEFINE_OPTIONAL_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv)        \
     DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, KVER_INF, \
-                                   true)
+                                   OPTIONAL)
 
 // programs with no kernel version requirements
 #define DEFINE_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
-    DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, 0, KVER_INF, false)
+    DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, KVER_NONE, KVER_INF, \
+                                   MANDATORY)
 #define DEFINE_OPTIONAL_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
-    DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, 0, KVER_INF, true)
+    DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, KVER_NONE, KVER_INF, \
+                                   OPTIONAL)
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h b/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
index e7428b6..ef03c4d 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
@@ -114,6 +114,31 @@
 // BPF wants 8, but 32-bit x86 wants 4
 //_Static_assert(_Alignof(unsigned long long) == 8, "_Alignof unsigned long long != 8");
 
+
+// for maps:
+struct shared_bool { bool shared; };
+#define PRIVATE ((struct shared_bool){ .shared = false })
+#define SHARED ((struct shared_bool){ .shared = true })
+
+// for programs:
+struct optional_bool { bool optional; };
+#define MANDATORY ((struct optional_bool){ .optional = false })
+#define OPTIONAL ((struct optional_bool){ .optional = true })
+
+// for both maps and programs:
+struct ignore_on_eng_bool { bool ignore_on_eng; };
+#define LOAD_ON_ENG ((struct ignore_on_eng_bool){ .ignore_on_eng = false })
+#define IGNORE_ON_ENG ((struct ignore_on_eng_bool){ .ignore_on_eng = true })
+
+struct ignore_on_user_bool { bool ignore_on_user; };
+#define LOAD_ON_USER ((struct ignore_on_user_bool){ .ignore_on_user = false })
+#define IGNORE_ON_USER ((struct ignore_on_user_bool){ .ignore_on_user = true })
+
+struct ignore_on_userdebug_bool { bool ignore_on_userdebug; };
+#define LOAD_ON_USERDEBUG ((struct ignore_on_userdebug_bool){ .ignore_on_userdebug = false })
+#define IGNORE_ON_USERDEBUG ((struct ignore_on_userdebug_bool){ .ignore_on_userdebug = true })
+
+
 // Length of strings (incl. selinux_context and pin_subdir)
 // in the bpf_map_def and bpf_prog_def structs.
 //
diff --git a/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h b/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h
index 13f7cb3..9995cb9 100644
--- a/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h
+++ b/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h
@@ -44,6 +44,11 @@
     return syscall(__NR_bpf, cmd, &attr, sizeof(attr));
 }
 
+// this version is meant for use with cmd's which mutate the argument
+inline int bpf(enum bpf_cmd cmd, bpf_attr *attr) {
+    return syscall(__NR_bpf, cmd, attr, sizeof(*attr));
+}
+
 inline int createMap(bpf_map_type map_type, uint32_t key_size, uint32_t value_size,
                      uint32_t max_entries, uint32_t map_flags) {
     return bpf(BPF_MAP_CREATE, {
@@ -160,6 +165,27 @@
                                 });
 }
 
+inline int queryProgram(const BPF_FD_TYPE cg_fd,
+                        enum bpf_attach_type attach_type,
+                        __u32 query_flags = 0,
+                        __u32 attach_flags = 0) {
+    int prog_id = -1;  // equivalent to an array of one integer.
+    bpf_attr arg = {
+            .query = {
+                    .target_fd = BPF_FD_TO_U32(cg_fd),
+                    .attach_type = attach_type,
+                    .query_flags = query_flags,
+                    .attach_flags = attach_flags,
+                    .prog_ids = ptr_to_u64(&prog_id),  // pointer to output array
+                    .prog_cnt = 1,  // in: space - nr of ints in the array, out: used
+            }
+    };
+    int v = bpf(BPF_PROG_QUERY, &arg);
+    if (v) return v;  // error case
+    if (!arg.query.prog_cnt) return 0;  // no program, kernel never returns zero id
+    return prog_id;  // return actual id
+}
+
 inline int detachSingleProgram(bpf_attach_type type, const BPF_FD_TYPE prog_fd,
                                const BPF_FD_TYPE cg_fd) {
     return bpf(BPF_PROG_DETACH, {
diff --git a/staticlibs/native/bpfmapjni/com_android_net_module_util_TcUtils.cpp b/staticlibs/native/bpfmapjni/com_android_net_module_util_TcUtils.cpp
index cb06afb..ab83da6 100644
--- a/staticlibs/native/bpfmapjni/com_android_net_module_util_TcUtils.cpp
+++ b/staticlibs/native/bpfmapjni/com_android_net_module_util_TcUtils.cpp
@@ -27,7 +27,7 @@
 }
 
 static jboolean com_android_net_module_util_TcUtils_isEthernet(JNIEnv *env,
-                                                               jobject clazz,
+                                                               jclass clazz,
                                                                jstring iface) {
   ScopedUtfChars interface(env, iface);
   bool result = false;
@@ -43,7 +43,7 @@
 // tc filter add dev .. in/egress prio 1 protocol ipv6/ip bpf object-pinned
 // /sys/fs/bpf/... direct-action
 static void com_android_net_module_util_TcUtils_tcFilterAddDevBpf(
-    JNIEnv *env, jobject clazz, jint ifIndex, jboolean ingress, jshort prio,
+    JNIEnv *env, jclass clazz, jint ifIndex, jboolean ingress, jshort prio,
     jshort proto, jstring bpfProgPath) {
   ScopedUtfChars pathname(env, bpfProgPath);
   int error = tcAddBpfFilter(ifIndex, ingress, prio, proto, pathname.c_str());
@@ -59,7 +59,7 @@
 //     action bpf object-pinned .. \
 //     drop
 static void com_android_net_module_util_TcUtils_tcFilterAddDevIngressPolice(
-    JNIEnv *env, jobject clazz, jint ifIndex, jshort prio, jshort proto,
+    JNIEnv *env, jclass clazz, jint ifIndex, jshort prio, jshort proto,
     jint rateInBytesPerSec, jstring bpfProgPath) {
   ScopedUtfChars pathname(env, bpfProgPath);
   int error = tcAddIngressPoliceFilter(ifIndex, prio, proto, rateInBytesPerSec,
@@ -74,7 +74,7 @@
 
 // tc filter del dev .. in/egress prio .. protocol ..
 static void com_android_net_module_util_TcUtils_tcFilterDelDev(
-    JNIEnv *env, jobject clazz, jint ifIndex, jboolean ingress, jshort prio,
+    JNIEnv *env, jclass clazz, jint ifIndex, jboolean ingress, jshort prio,
     jshort proto) {
   int error = tcDeleteFilter(ifIndex, ingress, prio, proto);
   if (error) {
@@ -86,7 +86,7 @@
 
 // tc qdisc add dev .. clsact
 static void com_android_net_module_util_TcUtils_tcQdiscAddDevClsact(JNIEnv *env,
-                                                                    jobject clazz,
+                                                                    jclass clazz,
                                                                     jint ifIndex) {
   int error = tcAddQdiscClsact(ifIndex);
   if (error) {
diff --git a/staticlibs/native/bpfutiljni/com_android_net_module_util_BpfUtils.cpp b/staticlibs/native/bpfutiljni/com_android_net_module_util_BpfUtils.cpp
index 0f2ebbd..cf09379 100644
--- a/staticlibs/native/bpfutiljni/com_android_net_module_util_BpfUtils.cpp
+++ b/staticlibs/native/bpfutiljni/com_android_net_module_util_BpfUtils.cpp
@@ -32,7 +32,7 @@
 
 // If attach fails throw error and return false.
 static jboolean com_android_net_module_util_BpfUtil_attachProgramToCgroup(JNIEnv *env,
-        jobject clazz, jint type, jstring bpfProgPath, jstring cgroupPath, jint flags) {
+        jclass clazz, jint type, jstring bpfProgPath, jstring cgroupPath, jint flags) {
 
     ScopedUtfChars dirPath(env, cgroupPath);
     unique_fd cg_fd(open(dirPath.c_str(), O_DIRECTORY | O_RDONLY | O_CLOEXEC));
@@ -62,7 +62,7 @@
 
 // If detach fails throw error and return false.
 static jboolean com_android_net_module_util_BpfUtil_detachProgramFromCgroup(JNIEnv *env,
-        jobject clazz, jint type, jstring cgroupPath) {
+        jclass clazz, jint type, jstring cgroupPath) {
 
     ScopedUtfChars dirPath(env, cgroupPath);
     unique_fd cg_fd(open(dirPath.c_str(), O_DIRECTORY | O_RDONLY | O_CLOEXEC));
@@ -83,7 +83,7 @@
 
 // If detach single program fails throw error and return false.
 static jboolean com_android_net_module_util_BpfUtil_detachSingleProgramFromCgroup(JNIEnv *env,
-        jobject clazz, jint type, jstring bpfProgPath, jstring cgroupPath) {
+        jclass clazz, jint type, jstring bpfProgPath, jstring cgroupPath) {
 
     ScopedUtfChars dirPath(env, cgroupPath);
     unique_fd cg_fd(open(dirPath.c_str(), O_DIRECTORY | O_RDONLY | O_CLOEXEC));
@@ -110,6 +110,29 @@
     return true;
 }
 
+static jint com_android_net_module_util_BpfUtil_getProgramIdFromCgroup(JNIEnv *env,
+        jclass clazz, jint type, jstring cgroupPath) {
+
+    ScopedUtfChars dirPath(env, cgroupPath);
+    unique_fd cg_fd(open(dirPath.c_str(), O_DIRECTORY | O_RDONLY | O_CLOEXEC));
+    if (cg_fd == -1) {
+        jniThrowExceptionFmt(env, "java/io/IOException",
+                             "Failed to open the cgroup directory %s: %s",
+                             dirPath.c_str(), strerror(errno));
+        return -1;
+    }
+
+    int id = bpf::queryProgram(cg_fd, (bpf_attach_type) type);
+    if (id < 0) {
+        jniThrowExceptionFmt(env, "java/io/IOException",
+                             "Failed to query bpf program %d at %s: %s",
+                             type, dirPath.c_str(), strerror(errno));
+        return -1;
+    }
+    return id;  // may return 0 meaning none
+}
+
+
 /*
  * JNI registration.
  */
@@ -121,6 +144,8 @@
         (void*) com_android_net_module_util_BpfUtil_detachProgramFromCgroup },
     { "native_detachSingleProgramFromCgroup", "(ILjava/lang/String;Ljava/lang/String;)Z",
         (void*) com_android_net_module_util_BpfUtil_detachSingleProgramFromCgroup },
+    { "native_getProgramIdFromCgroup", "(ILjava/lang/String;)I",
+        (void*) com_android_net_module_util_BpfUtil_getProgramIdFromCgroup },
 };
 
 int register_com_android_net_module_util_BpfUtils(JNIEnv* env, char const* class_name) {
diff --git a/staticlibs/native/netjniutils/Android.bp b/staticlibs/native/netjniutils/Android.bp
index 22fd1fa..ca3bbbc 100644
--- a/staticlibs/native/netjniutils/Android.bp
+++ b/staticlibs/native/netjniutils/Android.bp
@@ -31,8 +31,8 @@
         "-Werror",
         "-Wno-unused-parameter",
     ],
-    sdk_version: "29",
-    min_sdk_version: "29",
+    sdk_version: "30",
+    min_sdk_version: "30",
     apex_available: [
         "//apex_available:anyapex",
         "//apex_available:platform",
diff --git a/staticlibs/netd/Android.bp b/staticlibs/netd/Android.bp
index d135a1c..65b3b09 100644
--- a/staticlibs/netd/Android.bp
+++ b/staticlibs/netd/Android.bp
@@ -19,7 +19,7 @@
 java_library {
     name: "netd_aidl_interface-lateststable-java",
     sdk_version: "system_current",
-    min_sdk_version: "29",
+    min_sdk_version: "30",
     static_libs: [
         "netd_aidl_interface-V13-java",
     ],
@@ -38,7 +38,7 @@
     apex_available: [
         "com.android.resolv",
     ],
-    min_sdk_version: "29",
+    min_sdk_version: "30",
 }
 
 cc_library_static {
@@ -50,7 +50,7 @@
         "com.android.resolv",
         "com.android.tethering",
     ],
-    min_sdk_version: "29",
+    min_sdk_version: "30",
 }
 
 cc_defaults {
@@ -96,17 +96,17 @@
                 "com.android.tethering",
                 "com.android.wifi",
             ],
-            // this is part of updatable modules(NetworkStack) which targets 29(Q)
-            min_sdk_version: "29",
+            // this is part of updatable modules(NetworkStack) which targets 30(R)
+            min_sdk_version: "30",
         },
         ndk: {
             apex_available: [
                 "//apex_available:platform",
                 "com.android.tethering",
             ],
-            // This is necessary for the DnsResovler tests to run in Android Q.
-            // Soong would recognize this value and produce the Q compatible aidl library.
-            min_sdk_version: "29",
+            // This is necessary for the DnsResovler tests to run in Android R.
+            // Soong would recognize this value and produce the R compatible aidl library.
+            min_sdk_version: "30",
         },
     },
     versions_with_info: [
@@ -170,7 +170,7 @@
 java_library {
     name: "netd_event_listener_interface-lateststable-java",
     sdk_version: "system_current",
-    min_sdk_version: "29",
+    min_sdk_version: "30",
     static_libs: [
         "netd_event_listener_interface-V1-java",
     ],
@@ -194,7 +194,7 @@
                 "//apex_available:platform",
                 "com.android.resolv",
             ],
-            min_sdk_version: "29",
+            min_sdk_version: "30",
         },
         java: {
             apex_available: [
@@ -202,7 +202,7 @@
                 "com.android.wifi",
                 "com.android.tethering",
             ],
-            min_sdk_version: "29",
+            min_sdk_version: "30",
         },
     },
     versions_with_info: [
diff --git a/staticlibs/netd/libnetdutils/Android.bp b/staticlibs/netd/libnetdutils/Android.bp
index 3169033..fdb9380 100644
--- a/staticlibs/netd/libnetdutils/Android.bp
+++ b/staticlibs/netd/libnetdutils/Android.bp
@@ -40,7 +40,7 @@
         "com.android.resolv",
         "com.android.tethering",
     ],
-    min_sdk_version: "29",
+    min_sdk_version: "30",
 }
 
 cc_test {
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index 40371e6..031e52f 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -9,7 +9,7 @@
 android_library {
     name: "NetworkStaticLibTestsLib",
     srcs: ["src/**/*.java","src/**/*.kt"],
-    min_sdk_version: "29",
+    min_sdk_version: "30",
     defaults: ["framework-connectivity-test-defaults"],
     static_libs: [
         "androidx.test.rules",
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
index 5a96bcb..06b3e2f 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
@@ -71,6 +71,10 @@
 public class DeviceConfigUtilsTest {
     private static final String TEST_NAME_SPACE = "connectivity";
     private static final String TEST_EXPERIMENT_FLAG = "experiment_flag";
+    private static final String CORE_NETWORKING_TRUNK_STABLE_NAMESPACE = "android_core_networking";
+    private static final String TEST_TRUNK_STABLE_FLAG = "trunk_stable_feature";
+    private static final String TEST_CORE_NETWORKING_TRUNK_STABLE_FLAG_PROPERTY =
+            "com.android.net.flags.trunk_stable_feature";
     private static final int TEST_FLAG_VALUE = 28;
     private static final String TEST_FLAG_VALUE_STRING = "28";
     private static final int TEST_DEFAULT_FLAG_VALUE = 0;
@@ -228,27 +232,57 @@
     }
 
     @Test
-    public void testIsNetworkStackFeatureEnabled() {
+    public void testIsFeatureEnabled() {
         doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY,
                 TEST_EXPERIMENT_FLAG));
-        assertTrue(DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
-    }
-
-    @Test
-    public void testIsTetheringFeatureEnabled() {
         doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
                 TEST_EXPERIMENT_FLAG));
+        assertTrue(DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
         assertTrue(DeviceConfigUtils.isTetheringFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
     }
-
     @Test
-    public void testFeatureDefaultEnabled() {
+    public void testIsFeatureEnabledFeatureDefaultDisabled() throws Exception {
         doReturn(null).when(() -> DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY,
                 TEST_EXPERIMENT_FLAG));
         doReturn(null).when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
                 TEST_EXPERIMENT_FLAG));
         assertFalse(DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
         assertFalse(DeviceConfigUtils.isTetheringFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
+
+        // If the flag is unset, package info is not queried
+        verify(mContext, never()).getPackageManager();
+        verify(mContext, never()).getPackageName();
+        verify(mPm, never()).getPackageInfo(anyString(), anyInt());
+    }
+
+    @Test
+    public void testIsFeatureEnabledFeatureForceEnabled() throws Exception {
+        doReturn("1").when(() -> DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY,
+                TEST_EXPERIMENT_FLAG));
+        doReturn("1").when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
+                TEST_EXPERIMENT_FLAG));
+        assertTrue(DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
+        assertTrue(DeviceConfigUtils.isTetheringFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
+
+        // If the feature is force enabled, package info is not queried
+        verify(mContext, never()).getPackageManager();
+        verify(mContext, never()).getPackageName();
+        verify(mPm, never()).getPackageInfo(anyString(), anyInt());
+    }
+
+    @Test
+    public void testIsFeatureEnabledFeatureForceDisabled() throws Exception {
+        doReturn("-1").when(() -> DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY,
+                TEST_EXPERIMENT_FLAG));
+        doReturn("-1").when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
+                TEST_EXPERIMENT_FLAG));
+        assertFalse(DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
+        assertFalse(DeviceConfigUtils.isTetheringFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
+
+        // If the feature is force disabled, package info is not queried
+        verify(mContext, never()).getPackageManager();
+        verify(mContext, never()).getPackageName();
+        verify(mPm, never()).getPackageInfo(anyString(), anyInt());
     }
 
     @Test
@@ -271,15 +305,12 @@
         assertFalse(DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
         assertFalse(DeviceConfigUtils.isTetheringFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
 
-        // Follow defaultEnabled if the flag is not set
+        // If the flag is not set feature is disabled
         doReturn(null).when(() -> DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY,
                 TEST_EXPERIMENT_FLAG));
         doReturn(null).when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
                 TEST_EXPERIMENT_FLAG));
-        assertFalse(DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG,
-                false /* defaultEnabled */));
-        assertTrue(DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG,
-                true /* defaultEnabled */));
+        assertFalse(DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
         assertFalse(DeviceConfigUtils.isTetheringFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
     }
 
@@ -415,25 +446,86 @@
     }
 
     @Test
-    public void testIsTetheringFeatureNotChickenedOut() throws Exception {
-        doReturn("0").when(() -> DeviceConfig.getProperty(
-                eq(NAMESPACE_TETHERING), eq(TEST_EXPERIMENT_FLAG)));
-        assertTrue(DeviceConfigUtils.isTetheringFeatureNotChickenedOut(TEST_EXPERIMENT_FLAG));
-
-        doReturn(TEST_FLAG_VALUE_STRING).when(
-                () -> DeviceConfig.getProperty(eq(NAMESPACE_TETHERING), eq(TEST_EXPERIMENT_FLAG)));
-        assertFalse(DeviceConfigUtils.isTetheringFeatureNotChickenedOut(TEST_EXPERIMENT_FLAG));
+    public void testIsFeatureNotChickenedOut() {
+        doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY,
+                TEST_EXPERIMENT_FLAG));
+        doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
+                TEST_EXPERIMENT_FLAG));
+        assertTrue(DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
+                mContext, TEST_EXPERIMENT_FLAG));
+        assertTrue(DeviceConfigUtils.isNetworkStackFeatureNotChickenedOut(
+                mContext, TEST_EXPERIMENT_FLAG));
     }
 
     @Test
-    public void testIsNetworkStackFeatureNotChickenedOut() throws Exception {
-        doReturn("0").when(() -> DeviceConfig.getProperty(
-                eq(NAMESPACE_CONNECTIVITY), eq(TEST_EXPERIMENT_FLAG)));
-        assertTrue(DeviceConfigUtils.isNetworkStackFeatureNotChickenedOut(TEST_EXPERIMENT_FLAG));
+    public void testIsFeatureNotChickenedOutFeatureDefaultEnabled() throws Exception {
+        doReturn(null).when(() -> DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY,
+                TEST_EXPERIMENT_FLAG));
+        doReturn(null).when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
+                TEST_EXPERIMENT_FLAG));
+        assertTrue(DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
+                mContext, TEST_EXPERIMENT_FLAG));
+        assertTrue(DeviceConfigUtils.isNetworkStackFeatureNotChickenedOut(
+                mContext, TEST_EXPERIMENT_FLAG));
 
-        doReturn(TEST_FLAG_VALUE_STRING).when(
-                () -> DeviceConfig.getProperty(eq(NAMESPACE_CONNECTIVITY),
-                                               eq(TEST_EXPERIMENT_FLAG)));
-        assertFalse(DeviceConfigUtils.isNetworkStackFeatureNotChickenedOut(TEST_EXPERIMENT_FLAG));
+        // If the flag is unset, package info is not queried
+        verify(mContext, never()).getPackageManager();
+        verify(mContext, never()).getPackageName();
+        verify(mPm, never()).getPackageInfo(anyString(), anyInt());
+    }
+
+    @Test
+    public void testIsFeatureNotChickenedOutFeatureForceEnabled() throws Exception {
+        doReturn("1").when(() -> DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY,
+                TEST_EXPERIMENT_FLAG));
+        doReturn("1").when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
+                TEST_EXPERIMENT_FLAG));
+        assertTrue(DeviceConfigUtils.isNetworkStackFeatureNotChickenedOut(
+                mContext, TEST_EXPERIMENT_FLAG));
+        assertTrue(DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
+                mContext, TEST_EXPERIMENT_FLAG));
+
+        // If the feature is force enabled, package info is not queried
+        verify(mContext, never()).getPackageManager();
+        verify(mContext, never()).getPackageName();
+        verify(mPm, never()).getPackageInfo(anyString(), anyInt());
+    }
+
+    @Test
+    public void testIsFeatureNotChickenedOutFeatureForceDisabled() throws Exception {
+        doReturn("-1").when(() -> DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY,
+                TEST_EXPERIMENT_FLAG));
+        doReturn("-1").when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
+                TEST_EXPERIMENT_FLAG));
+        assertFalse(DeviceConfigUtils.isNetworkStackFeatureNotChickenedOut(
+                mContext, TEST_EXPERIMENT_FLAG));
+        assertFalse(DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
+                mContext, TEST_EXPERIMENT_FLAG));
+
+        // If the feature is force disabled, package info is not queried
+        verify(mContext, never()).getPackageManager();
+        verify(mContext, never()).getPackageName();
+        verify(mPm, never()).getPackageInfo(anyString(), anyInt());
+    }
+
+    @Test
+    public void testIsCoreNetworkingTrunkStableFeatureEnabled() {
+        doReturn(null).when(() -> DeviceConfig.getProperty(
+                CORE_NETWORKING_TRUNK_STABLE_NAMESPACE,
+                TEST_CORE_NETWORKING_TRUNK_STABLE_FLAG_PROPERTY));
+        assertFalse(DeviceConfigUtils.isTrunkStableFeatureEnabled(
+                TEST_TRUNK_STABLE_FLAG));
+
+        doReturn("false").when(() -> DeviceConfig.getProperty(
+                CORE_NETWORKING_TRUNK_STABLE_NAMESPACE,
+                TEST_CORE_NETWORKING_TRUNK_STABLE_FLAG_PROPERTY));
+        assertFalse(DeviceConfigUtils.isTrunkStableFeatureEnabled(
+                TEST_TRUNK_STABLE_FLAG));
+
+        doReturn("true").when(() -> DeviceConfig.getProperty(
+                CORE_NETWORKING_TRUNK_STABLE_NAMESPACE,
+                TEST_CORE_NETWORKING_TRUNK_STABLE_FLAG_PROPERTY));
+        assertTrue(DeviceConfigUtils.isTrunkStableFeatureEnabled(
+                TEST_TRUNK_STABLE_FLAG));
     }
 }
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
index 3a72dd1..5e9b004 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
@@ -88,6 +88,11 @@
         if (SdkLevel.isAtLeastT() && targetSdk > 31) {
             var ctxt = new String(Files.readAllBytes(Paths.get("/proc/thread-self/attr/current")));
             assumeFalse("must not be platform app", ctxt.startsWith("u:r:platform_app:s0:"));
+            // NetworkStackCoverageTests uses the same UID with NetworkStack module, which
+            // still has the permission to send RTM_GETNEIGH message (sepolicy just blocks the
+            // access from untrusted_apps), also exclude the NetworkStackCoverageTests.
+            assumeFalse("network_stack context is expected to have permission to send RTM_GETNEIGH",
+                    ctxt.startsWith("u:r:network_stack:s0"));
             try {
                 NetlinkUtils.sendMessage(fd, req, 0, req.length, TEST_TIMEOUT_MS);
                 fail("RTM_GETNEIGH is not allowed for apps targeting SDK > 31 on T+ platforms,"
diff --git a/staticlibs/testutils/app/connectivitychecker/Android.bp b/staticlibs/testutils/app/connectivitychecker/Android.bp
index f7118cf..049ec9e 100644
--- a/staticlibs/testutils/app/connectivitychecker/Android.bp
+++ b/staticlibs/testutils/app/connectivitychecker/Android.bp
@@ -20,9 +20,9 @@
     name: "ConnectivityTestPreparer",
     srcs: ["src/**/*.kt"],
     sdk_version: "system_current",
-    // Allow running the test on any device with SDK Q+, even when built from a branch that uses
+    // Allow running the test on any device with SDK R+, even when built from a branch that uses
     // an unstable SDK, by targeting a stable SDK regardless of the build SDK.
-    min_sdk_version: "29",
+    min_sdk_version: "30",
     target_sdk_version: "30",
     static_libs: [
         "androidx.test.rules",
diff --git a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
index f1f0975..d75d9ca 100644
--- a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
+++ b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
@@ -18,17 +18,10 @@
 
 import android.content.pm.PackageManager.FEATURE_TELEPHONY
 import android.content.pm.PackageManager.FEATURE_WIFI
-import android.net.ConnectivityManager
-import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
-import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
-import android.net.NetworkRequest
 import android.telephony.TelephonyManager
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.testutils.ConnectUtil
-import com.android.testutils.RecorderCallback
-import com.android.testutils.TestableNetworkCallback
-import com.android.testutils.tryTest
 import kotlin.test.assertTrue
 import kotlin.test.fail
 import org.junit.Test
@@ -36,8 +29,9 @@
 
 @RunWith(AndroidJUnit4::class)
 class ConnectivityCheckTest {
-    val context by lazy { InstrumentationRegistry.getInstrumentation().context }
-    val pm by lazy { context.packageManager }
+    private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
+    private val pm by lazy { context.packageManager }
+    private val connectUtil by lazy { ConnectUtil(context) }
 
     @Test
     fun testCheckConnectivity() {
@@ -47,7 +41,7 @@
 
     private fun checkWifiSetup() {
         if (!pm.hasSystemFeature(FEATURE_WIFI)) return
-        ConnectUtil(context).ensureWifiConnected()
+        connectUtil.ensureWifiValidated()
     }
 
     private fun checkTelephonySetup() {
@@ -69,20 +63,6 @@
         assertTrue(tm.isDataConnectivityPossible,
             "The device is not setup with a SIM card that supports data connectivity. " +
                     commonError)
-        val cb = TestableNetworkCallback()
-        val cm = context.getSystemService(ConnectivityManager::class.java)
-                ?: fail("Could not get ConnectivityManager")
-        cm.requestNetwork(
-                NetworkRequest.Builder()
-                        .addTransportType(TRANSPORT_CELLULAR)
-                        .addCapability(NET_CAPABILITY_INTERNET).build(), cb)
-        tryTest {
-            cb.poll { it is RecorderCallback.CallbackEntry.Available }
-                    ?: fail("The device does not have mobile data available. Check that it is " +
-                            "setup with a SIM card that has a working data plan, and that the " +
-                            "APN configuration is valid.")
-        } cleanup {
-            cm.unregisterNetworkCallback(cb)
-        }
+        connectUtil.ensureCellularValidated()
     }
 }
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
index 71f7877..b1d64f8 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
@@ -23,6 +23,9 @@
 import android.content.IntentFilter
 import android.net.ConnectivityManager
 import android.net.Network
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
 import android.net.NetworkCapabilities.TRANSPORT_WIFI
 import android.net.NetworkRequest
 import android.net.wifi.ScanResult
@@ -33,6 +36,7 @@
 import android.util.Log
 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
 import com.android.testutils.RecorderCallback.CallbackEntry
+import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
 import java.util.concurrent.CompletableFuture
 import java.util.concurrent.TimeUnit
 import kotlin.test.assertNotNull
@@ -56,13 +60,35 @@
     private val wifiManager = context.getSystemService(WifiManager::class.java)
             ?: fail("Could not find WifiManager")
 
-    fun ensureWifiConnected(): Network {
-        val callback = TestableNetworkCallback()
+    fun ensureWifiConnected(): Network = ensureWifiConnected(requireValidated = false)
+    fun ensureWifiValidated(): Network = ensureWifiConnected(requireValidated = true)
+
+    fun ensureCellularValidated(): Network {
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(
+            NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_INTERNET).build(), cb)
+        return tryTest {
+            val errorMsg = "The device does not have mobile data available. Check that it is " +
+                    "setup with a SIM card that has a working data plan, that the APN " +
+                    "configuration is valid, and that the device can access the internet through " +
+                    "mobile data."
+            cb.eventuallyExpect<CapabilitiesChanged>(errorMsg) {
+                it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
+            }.network
+        } cleanup {
+            cm.unregisterNetworkCallback(cb)
+        }
+    }
+
+    private fun ensureWifiConnected(requireValidated: Boolean): Network {
+        val callback = TestableNetworkCallback(timeoutMs = WIFI_CONNECT_TIMEOUT_MS)
         cm.registerNetworkCallback(NetworkRequest.Builder()
                 .addTransportType(TRANSPORT_WIFI)
                 .build(), callback)
 
-        try {
+        return tryTest {
             val connInfo = wifiManager.connectionInfo
             Log.d(TAG, "connInfo=" + connInfo)
             if (connInfo == null || connInfo.networkId == -1) {
@@ -73,12 +99,19 @@
                 val config = getOrCreateWifiConfiguration()
                 connectToWifiConfig(config)
             }
-            val cb = callback.poll(WIFI_CONNECT_TIMEOUT_MS) { it is CallbackEntry.Available }
-            assertNotNull(cb, "Could not connect to a wifi access point within " +
-                    "$WIFI_CONNECT_TIMEOUT_MS ms. Check that the test device has a wifi network " +
-                    "configured, and that the test access point is functioning properly.")
-            return cb.network
-        } finally {
+            val errorMsg = if (requireValidated) {
+                "The wifi access point did not have access to the internet after " +
+                        "$WIFI_CONNECT_TIMEOUT_MS ms. Check that it has a working connection."
+            } else {
+                "Could not connect to a wifi access point within $WIFI_CONNECT_TIMEOUT_MS ms. " +
+                        "Check that the test device has a wifi network configured, and that the " +
+                        "test access point is functioning properly."
+            }
+            val cb = callback.eventuallyExpect<CapabilitiesChanged>(errorMsg) {
+                (!requireValidated || it.caps.hasCapability(NET_CAPABILITY_VALIDATED))
+            }
+            cb.network
+        } cleanup {
             cm.unregisterNetworkCallback(callback)
         }
     }
@@ -201,3 +234,10 @@
         }
     }
 }
+
+private inline fun <reified T : CallbackEntry> TestableNetworkCallback.eventuallyExpect(
+    errorMsg: String,
+    crossinline predicate: (T) -> Boolean = { true }
+): T = history.poll(defaultTimeoutMs, mark) { it is T && predicate(it) }.also {
+    assertNotNull(it, errorMsg)
+} as T
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
index 2e73666..2d281fd 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
@@ -19,6 +19,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import java.lang.reflect.Modifier
 import org.junit.runner.Description
 import org.junit.runner.Runner
 import org.junit.runner.manipulation.Filter
@@ -27,7 +28,7 @@
 import org.junit.runner.manipulation.Sortable
 import org.junit.runner.manipulation.Sorter
 import org.junit.runner.notification.RunNotifier
-import kotlin.jvm.Throws
+import org.junit.runners.Parameterized
 
 /**
  * A runner that can skip tests based on the development SDK as defined in [DevSdkIgnoreRule].
@@ -41,6 +42,9 @@
  * the whole class if they do not match the development SDK as defined in [DevSdkIgnoreRule].
  * Otherwise, it will delegate to [AndroidJUnit4] to run the test as usual.
  *
+ * This class automatically uses the Parameterized runner as its base runner when needed, so the
+ * @Parameterized.Parameters annotation and its friends can be used in tests using this runner.
+ *
  * Example usage:
  *
  *     @RunWith(DevSdkIgnoreRunner::class)
@@ -48,13 +52,34 @@
  *     class MyTestClass { ... }
  */
 class DevSdkIgnoreRunner(private val klass: Class<*>) : Runner(), Filterable, Sortable {
-    private val baseRunner = klass.let {
+    // Inference correctly infers Runner & Filterable & Sortable for |baseRunner|, but the
+    // Java bytecode doesn't have a way to express this. Give this type a name by wrapping it.
+    private class RunnerWrapper<T>(private val wrapped: T) :
+            Runner(), Filterable by wrapped, Sortable by wrapped
+            where T : Runner, T : Filterable, T : Sortable {
+        override fun getDescription(): Description = wrapped.description
+        override fun run(notifier: RunNotifier?) = wrapped.run(notifier)
+    }
+
+    private val baseRunner: RunnerWrapper<*>? = klass.let {
         val ignoreAfter = it.getAnnotation(IgnoreAfter::class.java)
         val ignoreUpTo = it.getAnnotation(IgnoreUpTo::class.java)
 
-        if (isDevSdkInRange(ignoreUpTo, ignoreAfter)) AndroidJUnit4(klass) else null
+        if (!isDevSdkInRange(ignoreUpTo, ignoreAfter)) {
+            null
+        } else if (it.hasParameterizedMethod()) {
+            // Parameterized throws if there is no static method annotated with @Parameters, which
+            // isn't too useful. Use it if there are, otherwise use its base AndroidJUnit4 runner.
+            RunnerWrapper(Parameterized(klass))
+        } else {
+            RunnerWrapper(AndroidJUnit4(klass))
+        }
     }
 
+    private fun <T> Class<T>.hasParameterizedMethod(): Boolean = methods.any {
+        Modifier.isStatic(it.modifiers) &&
+                it.isAnnotationPresent(Parameterized.Parameters::class.java) }
+
     override fun run(notifier: RunNotifier) {
         if (baseRunner != null) {
             baseRunner.run(notifier)
@@ -88,4 +113,4 @@
     override fun sort(sorter: Sorter?) {
         baseRunner?.sort(sorter)
     }
-}
\ No newline at end of file
+}
diff --git a/staticlibs/testutils/host/com/android/testutils/ConnectivityTestTargetPreparer.kt b/staticlibs/testutils/host/com/android/testutils/ConnectivityTestTargetPreparer.kt
index 3fc74aa..eb94781 100644
--- a/staticlibs/testutils/host/com/android/testutils/ConnectivityTestTargetPreparer.kt
+++ b/staticlibs/testutils/host/com/android/testutils/ConnectivityTestTargetPreparer.kt
@@ -32,6 +32,10 @@
 private const val CONNECTIVITY_CHECK_RUNNER_NAME = "androidx.test.runner.AndroidJUnitRunner"
 private const val IGNORE_CONN_CHECK_OPTION = "ignore-connectivity-check"
 
+// The default updater package names, which might be updating packages while the CTS
+// are running
+private val UPDATER_PKGS = arrayOf("com.google.android.gms", "com.android.vending")
+
 /**
  * A target preparer that sets up and verifies a device for connectivity tests.
  *
@@ -45,35 +49,42 @@
     @Option(name = IGNORE_CONN_CHECK_OPTION,
             description = "Disables the check for mobile data and wifi")
     private var ignoreConnectivityCheck = false
+    // The default value is never used, but false is a reasonable default
+    private var originalTestChainEnabled = false
+    private val originalUpdaterPkgsStatus = HashMap<String, Boolean>()
 
-    override fun setUp(testInformation: TestInformation) {
+    override fun setUp(testInfo: TestInformation) {
         if (isDisabled) return
-        disableGmsUpdate(testInformation)
-        runPreparerApk(testInformation)
+        disableGmsUpdate(testInfo)
+        originalTestChainEnabled = getTestChainEnabled(testInfo)
+        originalUpdaterPkgsStatus.putAll(getUpdaterPkgsStatus(testInfo))
+        setUpdaterNetworkingEnabled(testInfo, enableChain = true,
+                enablePkgs = UPDATER_PKGS.associateWith { false })
+        runPreparerApk(testInfo)
     }
 
-    private fun runPreparerApk(testInformation: TestInformation) {
+    private fun runPreparerApk(testInfo: TestInformation) {
         installer.setCleanApk(true)
         installer.addTestFileName(CONNECTIVITY_CHECKER_APK)
         installer.setShouldGrantPermission(true)
-        installer.setUp(testInformation)
+        installer.setUp(testInfo)
 
         val runner = DefaultRemoteAndroidTestRunner(
                 CONNECTIVITY_PKG_NAME,
                 CONNECTIVITY_CHECK_RUNNER_NAME,
-                testInformation.device.iDevice)
+                testInfo.device.iDevice)
         runner.runOptions = "--no-hidden-api-checks"
 
         val receiver = CollectingTestListener()
-        if (!testInformation.device.runInstrumentationTests(runner, receiver)) {
+        if (!testInfo.device.runInstrumentationTests(runner, receiver)) {
             throw TargetSetupError("Device state check failed to complete",
-                    testInformation.device.deviceDescriptor)
+                    testInfo.device.deviceDescriptor)
         }
 
         val runResult = receiver.currentRunResults
         if (runResult.isRunFailure) {
             throw TargetSetupError("Failed to check device state before the test: " +
-                    runResult.runFailureMessage, testInformation.device.deviceDescriptor)
+                    runResult.runFailureMessage, testInfo.device.deviceDescriptor)
         }
 
         val ignoredTestClasses = mutableSetOf<String>()
@@ -92,25 +103,50 @@
         if (errorMsg.isBlank()) return
 
         throw TargetSetupError("Device setup checks failed. Check the test bench: \n$errorMsg",
-                testInformation.device.deviceDescriptor)
+                testInfo.device.deviceDescriptor)
     }
 
-    private fun disableGmsUpdate(testInformation: TestInformation) {
+    private fun disableGmsUpdate(testInfo: TestInformation) {
         // This will be a no-op on devices without root (su) or not using gservices, but that's OK.
-        testInformation.device.executeShellCommand("su 0 am broadcast " +
+        testInfo.exec("su 0 am broadcast " +
                 "-a com.google.gservices.intent.action.GSERVICES_OVERRIDE " +
                 "-e finsky.play_services_auto_update_enabled false")
     }
 
-    private fun clearGmsUpdateOverride(testInformation: TestInformation) {
-        testInformation.device.executeShellCommand("su 0 am broadcast " +
+    private fun clearGmsUpdateOverride(testInfo: TestInformation) {
+        testInfo.exec("su 0 am broadcast " +
                 "-a com.google.gservices.intent.action.GSERVICES_OVERRIDE " +
                 "--esn finsky.play_services_auto_update_enabled")
     }
 
-    override fun tearDown(testInformation: TestInformation, e: Throwable?) {
+    private fun setUpdaterNetworkingEnabled(
+            testInfo: TestInformation,
+            enableChain: Boolean,
+            enablePkgs: Map<String, Boolean>
+    ) {
+        // Build.VERSION_CODES.S = 31 where this is not available, then do nothing.
+        if (testInfo.device.getApiLevel() < 31) return
+        testInfo.exec("cmd connectivity set-chain3-enabled $enableChain")
+        enablePkgs.forEach { (pkg, allow) ->
+            testInfo.exec("cmd connectivity set-package-networking-enabled $pkg $allow")
+        }
+    }
+
+    private fun getTestChainEnabled(testInfo: TestInformation) =
+            testInfo.exec("cmd connectivity get-chain3-enabled").contains("chain:enabled")
+
+    private fun getUpdaterPkgsStatus(testInfo: TestInformation) =
+            UPDATER_PKGS.associateWith { pkg ->
+                !testInfo.exec("cmd connectivity get-package-networking-enabled $pkg")
+                        .contains(":deny")
+            }
+
+    override fun tearDown(testInfo: TestInformation, e: Throwable?) {
         if (isTearDownDisabled) return
-        installer.tearDown(testInformation, e)
-        clearGmsUpdateOverride(testInformation)
+        installer.tearDown(testInfo, e)
+        setUpdaterNetworkingEnabled(testInfo,
+                enableChain = originalTestChainEnabled,
+                enablePkgs = originalUpdaterPkgsStatus)
+        clearGmsUpdateOverride(testInfo)
     }
 }
diff --git a/staticlibs/testutils/host/com/android/testutils/DisableConfigSyncTargetPreparer.kt b/staticlibs/testutils/host/com/android/testutils/DisableConfigSyncTargetPreparer.kt
index 63f05a6..bc00f3c 100644
--- a/staticlibs/testutils/host/com/android/testutils/DisableConfigSyncTargetPreparer.kt
+++ b/staticlibs/testutils/host/com/android/testutils/DisableConfigSyncTargetPreparer.kt
@@ -58,4 +58,4 @@
     }
 }
 
-private fun TestInformation.exec(cmd: String) = this.device.executeShellCommand(cmd)
\ No newline at end of file
+fun TestInformation.exec(cmd: String) = this.device.executeShellCommand(cmd)
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
index 5f321bb..d92fb01 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
@@ -199,7 +199,8 @@
     protected void tearDown() throws Exception {
         executeShellCommand("cmd netpolicy stop-watching");
         mServiceClient.unbind();
-        if (mLock.isHeld()) mLock.release();
+        final PowerManager.WakeLock lock = mLock;
+        if (null != lock && lock.isHeld()) lock.release();
     }
 
     protected int getUid(String packageName) throws Exception {
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index 1276d59..b86de25 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -55,7 +55,8 @@
         "junit-params",
         "modules-utils-build",
         "net-utils-framework-common",
-        "truth-prebuilt",
+        "truth",
+        "TetheringIntegrationTestsBaseLib",
     ],
 
     // uncomment when b/13249961 is fixed
@@ -73,7 +74,10 @@
 // devices.
 android_test {
     name: "CtsNetTestCases",
-    defaults: ["CtsNetTestCasesDefaults", "ConnectivityNextEnableDefaults"],
+    defaults: [
+        "CtsNetTestCasesDefaults",
+        "ConnectivityNextEnableDefaults",
+    ],
     static_libs: [
         "DhcpPacketLib",
         "NetworkStackApiCurrentShims",
@@ -128,7 +132,7 @@
 }
 
 android_test {
-    name: "CtsNetTestCasesMaxTargetSdk33",  // Must match CtsNetTestCasesMaxTargetSdk33 annotation.
+    name: "CtsNetTestCasesMaxTargetSdk33", // Must match CtsNetTestCasesMaxTargetSdk33 annotation.
     defaults: ["CtsNetTestCasesMaxTargetSdkDefaults"],
     target_sdk_version: "33",
     package_name: "android.net.cts.maxtargetsdk33",
@@ -136,17 +140,17 @@
 }
 
 android_test {
-    name: "CtsNetTestCasesMaxTargetSdk31",  // Must match CtsNetTestCasesMaxTargetSdk31 annotation.
+    name: "CtsNetTestCasesMaxTargetSdk31", // Must match CtsNetTestCasesMaxTargetSdk31 annotation.
     defaults: ["CtsNetTestCasesMaxTargetSdkDefaults"],
     target_sdk_version: "31",
-    package_name: "android.net.cts.maxtargetsdk31",  // CTS package names must be unique.
+    package_name: "android.net.cts.maxtargetsdk31", // CTS package names must be unique.
     instrumentation_target_package: "android.net.cts.maxtargetsdk31",
 }
 
 android_test {
-    name: "CtsNetTestCasesMaxTargetSdk30",  // Must match CtsNetTestCasesMaxTargetSdk30 annotation.
+    name: "CtsNetTestCasesMaxTargetSdk30", // Must match CtsNetTestCasesMaxTargetSdk30 annotation.
     defaults: ["CtsNetTestCasesMaxTargetSdkDefaults"],
     target_sdk_version: "30",
-    package_name: "android.net.cts.maxtargetsdk30",  // CTS package names must be unique.
+    package_name: "android.net.cts.maxtargetsdk30", // CTS package names must be unique.
     instrumentation_target_package: "android.net.cts.maxtargetsdk30",
 }
diff --git a/tests/cts/net/api23Test/Android.bp b/tests/cts/net/api23Test/Android.bp
index 9b81a56..1f1dd5d 100644
--- a/tests/cts/net/api23Test/Android.bp
+++ b/tests/cts/net/api23Test/Android.bp
@@ -41,7 +41,7 @@
         "mockwebserver",
         "junit",
         "junit-params",
-        "truth-prebuilt",
+        "truth",
     ],
 
     platform_apis: true,
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
index 60befca..e0fe929 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
@@ -38,9 +38,7 @@
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
 import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
 
-import static com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity;
 import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
-import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
 import static com.android.testutils.Cleanup.testAndCleanup;
 
 import static org.junit.Assert.assertEquals;
@@ -82,6 +80,8 @@
 
 import androidx.test.InstrumentationRegistry;
 
+import com.android.compatibility.common.util.SystemUtil;
+import com.android.compatibility.common.util.ThrowingRunnable;
 import com.android.internal.telephony.uicc.IccUtils;
 import com.android.internal.util.ArrayUtils;
 import com.android.modules.utils.build.SdkLevel;
@@ -99,6 +99,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.Callable;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
@@ -143,7 +144,7 @@
     // runWithShellPermissionIdentity, and callWithShellPermissionIdentity ensures Shell Permission
     // is not interrupted by another operation (which would drop all previously adopted
     // permissions).
-    private Object mShellPermissionsIdentityLock = new Object();
+    private final Object mShellPermissionsIdentityLock = new Object();
 
     private Context mContext;
     private ConnectivityManager mConnectivityManager;
@@ -177,6 +178,20 @@
         Log.i(TAG, "Waited for broadcast idle for " + (SystemClock.elapsedRealtime() - st) + "ms");
     }
 
+    private void runWithShellPermissionIdentity(ThrowingRunnable runnable,
+            String... permissions) {
+        synchronized (mShellPermissionsIdentityLock) {
+            SystemUtil.runWithShellPermissionIdentity(runnable, permissions);
+        }
+    }
+
+    private <T> T callWithShellPermissionIdentity(Callable<T> callable, String... permissions)
+            throws Exception {
+        synchronized (mShellPermissionsIdentityLock) {
+            return SystemUtil.callWithShellPermissionIdentity(callable, permissions);
+        }
+    }
+
     @Before
     public void setUp() throws Exception {
         mContext = InstrumentationRegistry.getContext();
@@ -199,7 +214,7 @@
             runWithShellPermissionIdentity(() -> {
                 final TestNetworkManager tnm = mContext.getSystemService(TestNetworkManager.class);
                 tnm.teardownTestNetwork(mTestNetwork);
-            });
+            }, android.Manifest.permission.MANAGE_TEST_NETWORKS);
             mTestNetwork = null;
         }
 
@@ -249,7 +264,7 @@
             doBroadcastCarrierConfigsAndVerifyOnConnectivityReportAvailable(
                     subId, carrierConfigReceiver, testNetworkCallback);
         }, () -> {
-            runWithShellPermissionIdentity(
+                runWithShellPermissionIdentity(
                     () -> mCarrierConfigManager.overrideConfig(subId, null),
                     android.Manifest.permission.MODIFY_PHONE_STATE);
             mConnectivityManager.unregisterNetworkCallback(testNetworkCallback);
@@ -276,24 +291,20 @@
                 CarrierConfigManager.KEY_CARRIER_CERTIFICATE_STRING_ARRAY,
                 new String[] {getCertHashForThisPackage()});
 
-        synchronized (mShellPermissionsIdentityLock) {
-            runWithShellPermissionIdentity(
-                    () -> {
-                        mCarrierConfigManager.overrideConfig(subId, carrierConfigs);
-                        mCarrierConfigManager.notifyConfigChangedForSubId(subId);
-                    },
-                    android.Manifest.permission.MODIFY_PHONE_STATE);
-        }
+        runWithShellPermissionIdentity(
+                () -> {
+                    mCarrierConfigManager.overrideConfig(subId, carrierConfigs);
+                    mCarrierConfigManager.notifyConfigChangedForSubId(subId);
+                },
+                android.Manifest.permission.MODIFY_PHONE_STATE);
 
         // TODO(b/157779832): This should use android.permission.CHANGE_NETWORK_STATE. However, the
         // shell does not have CHANGE_NETWORK_STATE, so use CONNECTIVITY_INTERNAL until the shell
         // permissions are updated.
-        synchronized (mShellPermissionsIdentityLock) {
-            runWithShellPermissionIdentity(
-                    () -> mConnectivityManager.requestNetwork(
-                            CELLULAR_NETWORK_REQUEST, testNetworkCallback),
-                    android.Manifest.permission.CONNECTIVITY_INTERNAL);
-        }
+        runWithShellPermissionIdentity(
+                () -> mConnectivityManager.requestNetwork(
+                        CELLULAR_NETWORK_REQUEST, testNetworkCallback),
+                android.Manifest.permission.CONNECTIVITY_INTERNAL);
 
         final Network network = testNetworkCallback.waitForAvailable();
         assertNotNull(network);
@@ -494,7 +505,7 @@
                     final TestNetworkInterface tni = tnm.createTunInterface(new LinkAddress[0]);
                     tnm.setupTestNetwork(tni.getInterfaceName(), administratorUids, BINDER);
                     return tni;
-                });
+                }, android.Manifest.permission.MANAGE_TEST_NETWORKS);
     }
 
     private static class TestConnectivityDiagnosticsCallback
@@ -648,11 +659,9 @@
 
             final PersistableBundle carrierConfigs;
             try {
-                synchronized (mShellPermissionsIdentityLock) {
-                    carrierConfigs = callWithShellPermissionIdentity(
-                            () -> mCarrierConfigManager.getConfigForSubId(subId),
-                            android.Manifest.permission.READ_PHONE_STATE);
-                }
+                carrierConfigs = callWithShellPermissionIdentity(
+                        () -> mCarrierConfigManager.getConfigForSubId(subId),
+                        android.Manifest.permission.READ_PHONE_STATE);
             } catch (Exception exception) {
                 // callWithShellPermissionIdentity() threw an Exception - cache it and allow
                 // waitForCarrierConfigChanged() to throw it
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 3a76cc2..62614c1 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -47,6 +47,7 @@
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_RESTRICTED;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_STANDBY;
 import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
+import static android.net.ConnectivityManager.FIREWALL_RULE_DEFAULT;
 import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
 import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE;
 import static android.net.ConnectivityManager.TYPE_BLUETOOTH;
@@ -2723,7 +2724,8 @@
             // the network with the TEST transport. Also wait for validation here, in case there
             // is a bug that's only visible when the network is validated.
             setWifiMeteredStatusAndWait(ssid, true /* isMetered */, true /* waitForValidation */);
-            defaultCallback.expect(CallbackEntry.LOST, wifiNetwork, NETWORK_CALLBACK_TIMEOUT_MS);
+            defaultCallback.eventuallyExpect(CallbackEntry.LOST, NETWORK_CALLBACK_TIMEOUT_MS,
+                    l -> l.getNetwork().equals(wifiNetwork));
             waitForAvailable(defaultCallback, tnt.getNetwork());
             // Depending on if this device has cellular connectivity or not, multiple available
             // callbacks may be received. Eventually, metered Wi-Fi should be the final available
@@ -2908,7 +2910,6 @@
 
     @AppModeFull(reason = "WRITE_DEVICE_CONFIG permission can't be granted to instant apps")
     @Test
-    @SkipMainlinePresubmit(reason = "Out of SLO flakiness")
     public void testRejectPartialConnectivity_TearDownNetwork() throws Exception {
         assumeTrue(TestUtils.shouldTestSApis());
         assumeTrue("testAcceptPartialConnectivity_validatedNetwork cannot execute"
@@ -3591,6 +3592,15 @@
         }
     }
 
+    private void setUidFirewallRule(final int chain, final int uid, final int rule) {
+        try {
+            mCm.setUidFirewallRule(chain, uid, rule);
+        } catch (IllegalStateException ignored) {
+            // Removing match causes an exception when the rule entry for the uid does
+            // not exist. But this is fine and can be ignored.
+        }
+    }
+
     private static final boolean EXPECT_OPEN = false;
     private static final boolean EXPECT_CLOSE = true;
 
@@ -3599,6 +3609,8 @@
         runWithShellPermissionIdentity(() -> {
             // Firewall chain status will be restored after the test.
             final boolean wasChainEnabled = mCm.getFirewallChainEnabled(chain);
+            final int myUid = Process.myUid();
+            final int previousMyUidFirewallRule = mCm.getUidFirewallRule(chain, myUid);
             final int previousUidFirewallRule = mCm.getUidFirewallRule(chain, targetUid);
             final Socket socket = new Socket(TEST_HOST, HTTP_PORT);
             socket.setSoTimeout(NETWORK_REQUEST_TIMEOUT_MS);
@@ -3606,12 +3618,12 @@
                 mCm.setFirewallChainEnabled(chain, false /* enable */);
                 assertSocketOpen(socket);
 
-                try {
-                    mCm.setUidFirewallRule(chain, targetUid, rule);
-                } catch (IllegalStateException ignored) {
-                    // Removing match causes an exception when the rule entry for the uid does
-                    // not exist. But this is fine and can be ignored.
+                setUidFirewallRule(chain, targetUid, rule);
+                if (targetUid != myUid) {
+                    // If this test does not set rule on myUid, remove existing rule on myUid
+                    setUidFirewallRule(chain, myUid, FIREWALL_RULE_DEFAULT);
                 }
+
                 mCm.setFirewallChainEnabled(chain, true /* enable */);
 
                 if (expectClose) {
@@ -3624,11 +3636,9 @@
                     mCm.setFirewallChainEnabled(chain, wasChainEnabled);
                 }, /* cleanup */ () -> {
                     // Restore the uid firewall rule status
-                    try {
-                        mCm.setUidFirewallRule(chain, targetUid, previousUidFirewallRule);
-                    } catch (IllegalStateException ignored) {
-                        // Removing match causes an exception when the rule entry for the uid does
-                        // not exist. But this is fine and can be ignored.
+                    setUidFirewallRule(chain, targetUid, previousUidFirewallRule);
+                    if (targetUid != myUid) {
+                        setUidFirewallRule(chain, myUid, previousMyUidFirewallRule);
                     }
                 }, /* cleanup */ () -> {
                     socket.close();
diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
index 805dd65..6b7954a 100644
--- a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
+++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
@@ -22,6 +22,7 @@
 import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
 
 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+import static com.android.modules.utils.build.SdkLevel.isAtLeastU;
 import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
@@ -34,12 +35,14 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeTrue;
+import static org.junit.Assume.assumeFalse;
 
 import android.Manifest;
 import android.annotation.NonNull;
 import android.app.AppOpsManager;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.PackageManager;
 import android.net.ConnectivityManager;
 import android.net.Ikev2VpnProfile;
 import android.net.IpSecAlgorithm;
@@ -60,11 +63,7 @@
 
 import com.android.internal.util.HexDump;
 import com.android.networkstack.apishim.ConstantsShim;
-import com.android.networkstack.apishim.Ikev2VpnProfileBuilderShimImpl;
-import com.android.networkstack.apishim.Ikev2VpnProfileShimImpl;
 import com.android.networkstack.apishim.VpnManagerShimImpl;
-import com.android.networkstack.apishim.common.Ikev2VpnProfileBuilderShim;
-import com.android.networkstack.apishim.common.Ikev2VpnProfileShim;
 import com.android.networkstack.apishim.common.VpnManagerShim;
 import com.android.networkstack.apishim.common.VpnProfileStateShim;
 import com.android.testutils.DevSdkIgnoreRule;
@@ -75,6 +74,7 @@
 
 import org.bouncycastle.x509.X509V1CertificateGenerator;
 import org.junit.After;
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -203,6 +203,12 @@
         mUserCertKey = generateRandomCertAndKeyPair();
     }
 
+    @Before
+    public void setUp() {
+        assumeFalse("Skipping test because watches don't support VPN",
+            sContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH));
+    }
+
     @After
     public void tearDown() {
         for (TestableNetworkCallback callback : mCallbacksToUnregister) {
@@ -210,6 +216,15 @@
         }
         setAppop(AppOpsManager.OP_ACTIVATE_VPN, false);
         setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, false);
+
+        // Make sure the VpnProfile is not provisioned already.
+        sVpnMgr.stopProvisionedVpnProfile();
+
+        try {
+            sVpnMgr.startProvisionedVpnProfile();
+            fail("Expected SecurityException for missing consent");
+        } catch (SecurityException expected) {
+        }
     }
 
     /**
@@ -227,28 +242,25 @@
     }
 
     private Ikev2VpnProfile buildIkev2VpnProfileCommon(
-            @NonNull Ikev2VpnProfileBuilderShim builderShim, boolean isRestrictedToTestNetworks,
+            @NonNull Ikev2VpnProfile.Builder builder, boolean isRestrictedToTestNetworks,
             boolean requiresValidation, boolean automaticIpVersionSelectionEnabled,
             boolean automaticNattKeepaliveTimerEnabled) throws Exception {
 
-        builderShim.setBypassable(true)
+        builder.setBypassable(true)
                 .setAllowedAlgorithms(TEST_ALLOWED_ALGORITHMS)
                 .setProxy(TEST_PROXY_INFO)
                 .setMaxMtu(TEST_MTU)
                 .setMetered(false);
-        if (TestUtils.shouldTestTApis()) {
-            builderShim.setRequiresInternetValidation(requiresValidation);
+        if (isAtLeastT()) {
+            builder.setRequiresInternetValidation(requiresValidation);
         }
 
-        if (TestUtils.shouldTestUApis()) {
-            builderShim.setAutomaticIpVersionSelectionEnabled(automaticIpVersionSelectionEnabled);
-            builderShim.setAutomaticNattKeepaliveTimerEnabled(automaticNattKeepaliveTimerEnabled);
+        if (isAtLeastU()) {
+            builder.setAutomaticIpVersionSelectionEnabled(automaticIpVersionSelectionEnabled);
+            builder.setAutomaticNattKeepaliveTimerEnabled(automaticNattKeepaliveTimerEnabled);
         }
 
-        // Convert shim back to Ikev2VpnProfile.Builder since restrictToTestNetworks is a hidden
-        // method and is not defined in shims.
         // TODO: replace it in alternative way to remove the hidden method usage
-        final Ikev2VpnProfile.Builder builder = (Ikev2VpnProfile.Builder) builderShim.getBuilder();
         if (isRestrictedToTestNetworks) {
             builder.restrictToTestNetworks();
         }
@@ -264,16 +276,14 @@
                         ? IkeSessionTestUtils.IKE_PARAMS_V6 : IkeSessionTestUtils.IKE_PARAMS_V4,
                         IkeSessionTestUtils.CHILD_PARAMS);
 
-        final Ikev2VpnProfileBuilderShim builderShim =
-                Ikev2VpnProfileBuilderShimImpl.newInstance(params)
+        final Ikev2VpnProfile.Builder builder =
+                new Ikev2VpnProfile.Builder(params)
                         .setRequiresInternetValidation(requiresValidation)
                         .setProxy(TEST_PROXY_INFO)
                         .setMaxMtu(TEST_MTU)
                         .setMetered(false);
-        // Convert shim back to Ikev2VpnProfile.Builder since restrictToTestNetworks is a hidden
-        // method and is not defined in shims.
+
         // TODO: replace it in alternative way to remove the hidden method usage
-        final Ikev2VpnProfile.Builder builder = (Ikev2VpnProfile.Builder) builderShim.getBuilder();
         if (isRestrictedToTestNetworks) {
             builder.restrictToTestNetworks();
         }
@@ -283,8 +293,8 @@
     private Ikev2VpnProfile buildIkev2VpnProfilePsk(@NonNull String remote,
             boolean isRestrictedToTestNetworks, boolean requiresValidation)
             throws Exception {
-        final Ikev2VpnProfileBuilderShim builder =
-                Ikev2VpnProfileBuilderShimImpl.newInstance(remote, TEST_IDENTITY)
+        final Ikev2VpnProfile.Builder builder =
+                new Ikev2VpnProfile.Builder(remote, TEST_IDENTITY)
                         .setAuthPsk(TEST_PSK);
         return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
                 requiresValidation, false /* automaticIpVersionSelectionEnabled */,
@@ -293,8 +303,8 @@
 
     private Ikev2VpnProfile buildIkev2VpnProfileUsernamePassword(boolean isRestrictedToTestNetworks)
             throws Exception {
-        final Ikev2VpnProfileBuilderShim builder =
-                Ikev2VpnProfileBuilderShimImpl.newInstance(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
+        final Ikev2VpnProfile.Builder builder =
+                new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
                         .setAuthUsernamePassword(TEST_USER, TEST_PASSWORD, mServerRootCa);
         return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
                 false /* requiresValidation */, false /* automaticIpVersionSelectionEnabled */,
@@ -303,8 +313,8 @@
 
     private Ikev2VpnProfile buildIkev2VpnProfileDigitalSignature(boolean isRestrictedToTestNetworks)
             throws Exception {
-        final Ikev2VpnProfileBuilderShim builder =
-                Ikev2VpnProfileBuilderShimImpl.newInstance(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
+        final Ikev2VpnProfile.Builder builder =
+                new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
                         .setAuthDigitalSignature(
                                 mUserCertKey.cert, mUserCertKey.key, mServerRootCa);
         return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
@@ -347,7 +357,6 @@
     @Test
     public void testBuildIkev2VpnProfileWithIkeTunnelConnectionParams() throws Exception {
         assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
-        assumeTrue(TestUtils.shouldTestTApis());
 
         final IkeTunnelConnectionParams expectedParams = new IkeTunnelConnectionParams(
                 IkeSessionTestUtils.IKE_PARAMS_V6, IkeSessionTestUtils.CHILD_PARAMS);
@@ -567,7 +576,7 @@
         // regardless of its value. However, there is a race in Vpn(see b/228574221) that VPN may
         // misuse VPN network itself as the underlying network. The fix is not available without
         // SDK > T platform. Thus, verify this only on T+ platform.
-        if (!requiresValidation && TestUtils.shouldTestTApis()) {
+        if (!requiresValidation && isAtLeastT()) {
             cb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED, TIMEOUT_MS,
                     entry -> ((CallbackEntry.CapabilitiesChanged) entry).getCaps()
                             .hasCapability(NET_CAPABILITY_VALIDATED));
@@ -647,7 +656,6 @@
 
     @Test @IgnoreUpTo(SC_V2)
     public void testStartStopVpnProfileV4WithValidation() throws Exception {
-        assumeTrue(TestUtils.shouldTestTApis());
         doTestStartStopVpnProfile(false /* testIpv6Only */, true /* requiresValidation */,
                 false /* testSessionKey */, false /* testIkeTunConnParams */);
     }
@@ -660,35 +668,30 @@
 
     @Test @IgnoreUpTo(SC_V2)
     public void testStartStopVpnProfileV6WithValidation() throws Exception {
-        assumeTrue(TestUtils.shouldTestTApis());
         doTestStartStopVpnProfile(true /* testIpv6Only */, true /* requiresValidation */,
                 false /* testSessionKey */, false /* testIkeTunConnParams */);
     }
 
     @Test @IgnoreUpTo(SC_V2)
     public void testStartStopVpnProfileIkeTunConnParamsV4() throws Exception {
-        assumeTrue(TestUtils.shouldTestTApis());
         doTestStartStopVpnProfile(false /* testIpv6Only */, false /* requiresValidation */,
                 false /* testSessionKey */, true /* testIkeTunConnParams */);
     }
 
     @Test @IgnoreUpTo(SC_V2)
     public void testStartStopVpnProfileIkeTunConnParamsV4WithValidation() throws Exception {
-        assumeTrue(TestUtils.shouldTestTApis());
         doTestStartStopVpnProfile(false /* testIpv6Only */, true /* requiresValidation */,
                 false /* testSessionKey */, true /* testIkeTunConnParams */);
     }
 
     @Test @IgnoreUpTo(SC_V2)
     public void testStartStopVpnProfileIkeTunConnParamsV6() throws Exception {
-        assumeTrue(TestUtils.shouldTestTApis());
         doTestStartStopVpnProfile(true /* testIpv6Only */, false /* requiresValidation */,
                 false /* testSessionKey */, true /* testIkeTunConnParams */);
     }
 
     @Test @IgnoreUpTo(SC_V2)
     public void testStartStopVpnProfileIkeTunConnParamsV6WithValidation() throws Exception {
-        assumeTrue(TestUtils.shouldTestTApis());
         doTestStartStopVpnProfile(true /* testIpv6Only */, true /* requiresValidation */,
                 false /* testSessionKey */, true /* testIkeTunConnParams */);
     }
@@ -696,7 +699,6 @@
     @IgnoreUpTo(SC_V2)
     @Test
     public void testStartProvisionedVpnV4ProfileSession() throws Exception {
-        assumeTrue(TestUtils.shouldTestTApis());
         doTestStartStopVpnProfile(false /* testIpv6Only */, false /* requiresValidation */,
                 true /* testSessionKey */, false /* testIkeTunConnParams */);
     }
@@ -704,59 +706,44 @@
     @IgnoreUpTo(SC_V2)
     @Test
     public void testStartProvisionedVpnV6ProfileSession() throws Exception {
-        assumeTrue(TestUtils.shouldTestTApis());
         doTestStartStopVpnProfile(true /* testIpv6Only */, false /* requiresValidation */,
                 true /* testSessionKey */, false /* testIkeTunConnParams */);
     }
 
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
     @Test
     public void testBuildIkev2VpnProfileWithAutomaticNattKeepaliveTimerEnabled() throws Exception {
-        // Cannot use @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) because this test also requires API
-        // 34 shims, and @IgnoreUpTo does not check that.
-        assumeTrue(TestUtils.shouldTestUApis());
-
         final Ikev2VpnProfile profileWithDefaultValue = buildIkev2VpnProfilePsk(TEST_SERVER_ADDR_V6,
                 false /* isRestrictedToTestNetworks */, false /* requiresValidation */);
-        final Ikev2VpnProfileShim<Ikev2VpnProfile> shimWithDefaultValue =
-                Ikev2VpnProfileShimImpl.newInstance(profileWithDefaultValue);
-        assertFalse(shimWithDefaultValue.isAutomaticNattKeepaliveTimerEnabled());
+        assertFalse(profileWithDefaultValue.isAutomaticNattKeepaliveTimerEnabled());
 
-        final Ikev2VpnProfileBuilderShim builder =
-                Ikev2VpnProfileBuilderShimImpl.newInstance(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
+        final Ikev2VpnProfile.Builder builder =
+                new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
                         .setAuthPsk(TEST_PSK);
         final Ikev2VpnProfile profile = buildIkev2VpnProfileCommon(builder,
                 false /* isRestrictedToTestNetworks */,
                 false /* requiresValidation */,
                 false /* automaticIpVersionSelectionEnabled */,
                 true /* automaticNattKeepaliveTimerEnabled */);
-        final Ikev2VpnProfileShim<Ikev2VpnProfile> shim =
-                Ikev2VpnProfileShimImpl.newInstance(profile);
-        assertTrue(shim.isAutomaticNattKeepaliveTimerEnabled());
+        assertTrue(profile.isAutomaticNattKeepaliveTimerEnabled());
     }
 
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
     @Test
     public void testBuildIkev2VpnProfileWithAutomaticIpVersionSelectionEnabled() throws Exception {
-        // Cannot use @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) because this test also requires API
-        // 34 shims, and @IgnoreUpTo does not check that.
-        assumeTrue(TestUtils.shouldTestUApis());
-
         final Ikev2VpnProfile profileWithDefaultValue = buildIkev2VpnProfilePsk(TEST_SERVER_ADDR_V6,
                 false /* isRestrictedToTestNetworks */, false /* requiresValidation */);
-        final Ikev2VpnProfileShim<Ikev2VpnProfile> shimWithDefaultValue =
-                Ikev2VpnProfileShimImpl.newInstance(profileWithDefaultValue);
-        assertFalse(shimWithDefaultValue.isAutomaticIpVersionSelectionEnabled());
+        assertFalse(profileWithDefaultValue.isAutomaticIpVersionSelectionEnabled());
 
-        final Ikev2VpnProfileBuilderShim builder =
-                Ikev2VpnProfileBuilderShimImpl.newInstance(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
+        final Ikev2VpnProfile.Builder builder =
+                new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
                         .setAuthPsk(TEST_PSK);
         final Ikev2VpnProfile profile = buildIkev2VpnProfileCommon(builder,
                 false /* isRestrictedToTestNetworks */,
                 false /* requiresValidation */,
                 true /* automaticIpVersionSelectionEnabled */,
                 false /* automaticNattKeepaliveTimerEnabled */);
-        final Ikev2VpnProfileShim<Ikev2VpnProfile> shim =
-                Ikev2VpnProfileShimImpl.newInstance(profile);
-        assertTrue(shim.isAutomaticIpVersionSelectionEnabled());
+        assertTrue(profile.isAutomaticIpVersionSelectionEnabled());
     }
 
     private static class CertificateAndKey {
diff --git a/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt b/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt
new file mode 100644
index 0000000..eef3f87
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt
@@ -0,0 +1,302 @@
+/*
+ * 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.net.cts
+
+import android.net.DnsResolver
+import android.net.Network
+import android.net.nsd.NsdManager
+import android.net.nsd.NsdServiceInfo
+import android.os.Process
+import com.android.net.module.util.ArrayTrackRecord
+import com.android.net.module.util.DnsPacket
+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.TrackRecord
+import com.android.testutils.IPv6UdpFilter
+import com.android.testutils.TapPacketReader
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+private const val MDNS_REGISTRATION_TIMEOUT_MS = 10_000L
+private const val MDNS_PORT = 5353.toShort()
+const val MDNS_CALLBACK_TIMEOUT = 2000L
+const val MDNS_NO_CALLBACK_TIMEOUT_MS = 200L
+
+interface NsdEvent
+open class NsdRecord<T : NsdEvent> private constructor(
+    private val history: ArrayTrackRecord<T>,
+    private val expectedThreadId: Int? = null
+) : TrackRecord<T> by history {
+    constructor(expectedThreadId: Int? = null) : this(ArrayTrackRecord(), expectedThreadId)
+
+    val nextEvents = history.newReadHead()
+
+    override fun add(e: T): Boolean {
+        if (expectedThreadId != null) {
+            assertEquals(
+                expectedThreadId, Process.myTid(),
+                "Callback is running on the wrong thread"
+            )
+        }
+        return history.add(e)
+    }
+
+    inline fun <reified V : NsdEvent> expectCallbackEventually(
+        timeoutMs: Long = MDNS_CALLBACK_TIMEOUT,
+        crossinline predicate: (V) -> Boolean = { true }
+    ): V = nextEvents.poll(timeoutMs) { e -> e is V && predicate(e) } as V?
+        ?: fail("Callback for ${V::class.java.simpleName} not seen after $timeoutMs ms")
+
+    inline fun <reified V : NsdEvent> expectCallback(timeoutMs: Long = MDNS_CALLBACK_TIMEOUT): V {
+        val nextEvent = nextEvents.poll(timeoutMs)
+        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
+    }
+
+    inline fun assertNoCallback(timeoutMs: Long = MDNS_NO_CALLBACK_TIMEOUT_MS) {
+        val cb = nextEvents.poll(timeoutMs)
+        assertNull(cb, "Expected no callback but got $cb")
+    }
+}
+
+class NsdDiscoveryRecord(expectedThreadId: Int? = null) :
+    NsdManager.DiscoveryListener, NsdRecord<NsdDiscoveryRecord.DiscoveryEvent>(expectedThreadId) {
+    sealed class DiscoveryEvent : NsdEvent {
+        data class StartDiscoveryFailed(val serviceType: String, val errorCode: Int) :
+            DiscoveryEvent()
+
+        data class StopDiscoveryFailed(val serviceType: String, val errorCode: Int) :
+            DiscoveryEvent()
+
+        data class DiscoveryStarted(val serviceType: String) : DiscoveryEvent()
+        data class DiscoveryStopped(val serviceType: String) : DiscoveryEvent()
+        data class ServiceFound(val serviceInfo: NsdServiceInfo) : DiscoveryEvent()
+        data class ServiceLost(val serviceInfo: NsdServiceInfo) : DiscoveryEvent()
+    }
+
+    override fun onStartDiscoveryFailed(serviceType: String, err: Int) {
+        add(DiscoveryEvent.StartDiscoveryFailed(serviceType, err))
+    }
+
+    override fun onStopDiscoveryFailed(serviceType: String, err: Int) {
+        add(DiscoveryEvent.StopDiscoveryFailed(serviceType, err))
+    }
+
+    override fun onDiscoveryStarted(serviceType: String) {
+        add(DiscoveryEvent.DiscoveryStarted(serviceType))
+    }
+
+    override fun onDiscoveryStopped(serviceType: String) {
+        add(DiscoveryEvent.DiscoveryStopped(serviceType))
+    }
+
+    override fun onServiceFound(si: NsdServiceInfo) {
+        add(DiscoveryEvent.ServiceFound(si))
+    }
+
+    override fun onServiceLost(si: NsdServiceInfo) {
+        add(DiscoveryEvent.ServiceLost(si))
+    }
+
+    fun waitForServiceDiscovered(
+        serviceName: String,
+        serviceType: String,
+        expectedNetwork: Network? = null
+    ): NsdServiceInfo {
+        val serviceFound = expectCallbackEventually<DiscoveryEvent.ServiceFound> {
+            it.serviceInfo.serviceName == serviceName &&
+                    (expectedNetwork == null ||
+                            expectedNetwork == it.serviceInfo.network)
+        }.serviceInfo
+        // Discovered service types have a dot at the end
+        assertEquals("$serviceType.", serviceFound.serviceType)
+        return serviceFound
+    }
+}
+
+class NsdRegistrationRecord(expectedThreadId: Int? = null) : NsdManager.RegistrationListener,
+    NsdRecord<NsdRegistrationRecord.RegistrationEvent>(expectedThreadId) {
+    sealed class RegistrationEvent : NsdEvent {
+        abstract val serviceInfo: NsdServiceInfo
+
+        data class RegistrationFailed(
+            override val serviceInfo: NsdServiceInfo,
+            val errorCode: Int
+        ) : RegistrationEvent()
+
+        data class UnregistrationFailed(
+            override val serviceInfo: NsdServiceInfo,
+            val errorCode: Int
+        ) : RegistrationEvent()
+
+        data class ServiceRegistered(override val serviceInfo: NsdServiceInfo) :
+            RegistrationEvent()
+
+        data class ServiceUnregistered(override val serviceInfo: NsdServiceInfo) :
+            RegistrationEvent()
+    }
+
+    override fun onRegistrationFailed(si: NsdServiceInfo, err: Int) {
+        add(RegistrationEvent.RegistrationFailed(si, err))
+    }
+
+    override fun onUnregistrationFailed(si: NsdServiceInfo, err: Int) {
+        add(RegistrationEvent.UnregistrationFailed(si, err))
+    }
+
+    override fun onServiceRegistered(si: NsdServiceInfo) {
+        add(RegistrationEvent.ServiceRegistered(si))
+    }
+
+    override fun onServiceUnregistered(si: NsdServiceInfo) {
+        add(RegistrationEvent.ServiceUnregistered(si))
+    }
+}
+
+class NsdResolveRecord : NsdManager.ResolveListener,
+    NsdRecord<NsdResolveRecord.ResolveEvent>() {
+    sealed class ResolveEvent : NsdEvent {
+        data class ResolveFailed(val serviceInfo: NsdServiceInfo, val errorCode: Int) :
+            ResolveEvent()
+
+        data class ServiceResolved(val serviceInfo: NsdServiceInfo) : ResolveEvent()
+        data class ResolutionStopped(val serviceInfo: NsdServiceInfo) : ResolveEvent()
+        data class StopResolutionFailed(val serviceInfo: NsdServiceInfo, val errorCode: Int) :
+            ResolveEvent()
+    }
+
+    override fun onResolveFailed(si: NsdServiceInfo, err: Int) {
+        add(ResolveEvent.ResolveFailed(si, err))
+    }
+
+    override fun onServiceResolved(si: NsdServiceInfo) {
+        add(ResolveEvent.ServiceResolved(si))
+    }
+
+    override fun onResolutionStopped(si: NsdServiceInfo) {
+        add(ResolveEvent.ResolutionStopped(si))
+    }
+
+    override fun onStopResolutionFailed(si: NsdServiceInfo, err: Int) {
+        super.onStopResolutionFailed(si, err)
+        add(ResolveEvent.StopResolutionFailed(si, err))
+    }
+}
+
+class NsdServiceInfoCallbackRecord : NsdManager.ServiceInfoCallback,
+    NsdRecord<NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent>() {
+    sealed class ServiceInfoCallbackEvent : NsdEvent {
+        data class RegisterCallbackFailed(val errorCode: Int) : ServiceInfoCallbackEvent()
+        data class ServiceUpdated(val serviceInfo: NsdServiceInfo) : ServiceInfoCallbackEvent()
+        object ServiceUpdatedLost : ServiceInfoCallbackEvent()
+        object UnregisterCallbackSucceeded : ServiceInfoCallbackEvent()
+    }
+
+    override fun onServiceInfoCallbackRegistrationFailed(err: Int) {
+        add(ServiceInfoCallbackEvent.RegisterCallbackFailed(err))
+    }
+
+    override fun onServiceUpdated(si: NsdServiceInfo) {
+        add(ServiceInfoCallbackEvent.ServiceUpdated(si))
+    }
+
+    override fun onServiceLost() {
+        add(ServiceInfoCallbackEvent.ServiceUpdatedLost)
+    }
+
+    override fun onServiceInfoCallbackUnregistered() {
+        add(ServiceInfoCallbackEvent.UnregisterCallbackSucceeded)
+    }
+}
+
+private fun getMdnsPayload(packet: ByteArray) = packet.copyOfRange(
+    ETHER_HEADER_LEN + IPV6_HEADER_LEN + UDP_HEADER_LEN, packet.size)
+
+fun TapPacketReader.pollForMdnsPacket(
+    timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS,
+    predicate: (TestDnsPacket) -> Boolean
+): TestDnsPacket? {
+    val mdnsProbeFilter = IPv6UdpFilter(srcPort = MDNS_PORT, dstPort = MDNS_PORT).and {
+        val mdnsPayload = getMdnsPayload(it)
+        try {
+            predicate(TestDnsPacket(mdnsPayload))
+        } catch (e: DnsPacket.ParseException) {
+            false
+        }
+    }
+    return poll(timeoutMs, mdnsProbeFilter)?.let { TestDnsPacket(getMdnsPayload(it)) }
+}
+
+fun TapPacketReader.pollForProbe(
+    serviceName: String,
+    serviceType: String,
+    timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS
+): TestDnsPacket? = pollForMdnsPacket(timeoutMs) {
+    it.isProbeFor("$serviceName.$serviceType.local")
+}
+
+fun TapPacketReader.pollForAdvertisement(
+    serviceName: String,
+    serviceType: String,
+    timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS
+): TestDnsPacket? = pollForMdnsPacket(timeoutMs) {
+    it.isReplyFor("$serviceName.$serviceType.local")
+}
+
+fun TapPacketReader.pollForQuery(
+    recordName: String,
+    vararg requiredTypes: Int,
+    timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS
+): TestDnsPacket? = pollForMdnsPacket(timeoutMs) { it.isQueryFor(recordName, *requiredTypes) }
+
+fun TapPacketReader.pollForReply(
+    serviceName: String,
+    serviceType: String,
+    timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS
+): TestDnsPacket? = pollForMdnsPacket(timeoutMs) {
+    it.isReplyFor("$serviceName.$serviceType.local")
+}
+
+class TestDnsPacket(data: ByteArray) : DnsPacket(data) {
+    val header: DnsHeader
+        get() = mHeader
+    val records: Array<List<DnsRecord>>
+        get() = mRecords
+    fun isProbeFor(name: String): Boolean = mRecords[QDSECTION].any {
+        it.dName == name && it.nsType == DnsResolver.TYPE_ANY
+    }
+
+    fun isReplyFor(name: String): Boolean = mRecords[ANSECTION].any {
+        it.dName == name && it.nsType == DnsResolver.TYPE_SRV
+    }
+
+    fun isQueryFor(name: String, vararg requiredTypes: Int): Boolean = requiredTypes.all { type ->
+        mRecords[QDSECTION].any {
+            it.dName == name && it.nsType == type
+        }
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
index 637ed26..718ca6d 100644
--- a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
@@ -104,6 +104,23 @@
         verifyNoCapabilities(nr);
     }
 
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testForbiddenCapabilities() {
+        final NetworkRequest.Builder builder = new NetworkRequest.Builder();
+        builder.addForbiddenCapability(NET_CAPABILITY_MMS);
+        assertTrue(builder.build().hasForbiddenCapability(NET_CAPABILITY_MMS));
+        builder.removeForbiddenCapability(NET_CAPABILITY_MMS);
+        assertFalse(builder.build().hasCapability(NET_CAPABILITY_MMS));
+        builder.addCapability(NET_CAPABILITY_MMS);
+        assertFalse(builder.build().hasForbiddenCapability(NET_CAPABILITY_MMS));
+        assertTrue(builder.build().hasCapability(NET_CAPABILITY_MMS));
+        builder.addForbiddenCapability(NET_CAPABILITY_MMS);
+        assertTrue(builder.build().hasForbiddenCapability(NET_CAPABILITY_MMS));
+        assertFalse(builder.build().hasCapability(NET_CAPABILITY_MMS));
+        builder.clearCapabilities();
+        verifyNoCapabilities(builder.build());
+    }
+
     @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
     public void testTemporarilyNotMeteredCapability() {
         assertTrue(new NetworkRequest.Builder()
diff --git a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
index 7bccbde..6a019b7 100644
--- a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
@@ -387,6 +387,7 @@
                 now = System.currentTimeMillis();
             }
         }
+        mCm.unregisterNetworkCallback(callback);
         if (callback.success) {
             mNetworkInterfacesToTest[networkTypeIndex].setMetered(callback.metered);
             mNetworkInterfacesToTest[networkTypeIndex].setRoaming(callback.roaming);
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
new file mode 100644
index 0000000..f374181
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
@@ -0,0 +1,152 @@
+/*
+ * 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.net.cts
+
+import android.net.EthernetTetheringTestBase
+import android.net.LinkAddress
+import android.net.TestNetworkInterface
+import android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL
+import android.net.TetheringManager.TETHERING_ETHERNET
+import android.net.TetheringManager.TetheringRequest
+import android.net.nsd.NsdManager
+import android.os.Build
+import android.platform.test.annotations.AppModeFull
+import androidx.test.filters.SmallTest
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.TapPacketReader
+import com.android.testutils.tryTest
+import java.util.Random
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import org.junit.After
+import org.junit.Assume.assumeFalse
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@ConnectivityModuleTest
+@AppModeFull(reason = "WifiManager cannot be obtained in instant mode")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+class NsdManagerDownstreamTetheringTest : EthernetTetheringTestBase() {
+    private val nsdManager by lazy { context.getSystemService(NsdManager::class.java)!! }
+    private val serviceType = "_nmt%09d._tcp".format(Random().nextInt(1_000_000_000))
+
+    @Before
+    override fun setUp() {
+        super.setUp()
+        setIncludeTestInterfaces(true)
+    }
+
+    @After
+    override fun tearDown() {
+        super.tearDown()
+        setIncludeTestInterfaces(false)
+    }
+
+    @Test
+    fun testMdnsDiscoveryCanSendPacketOnLocalOnlyDownstreamTetheringInterface() {
+        assumeFalse(isInterfaceForTetheringAvailable)
+
+        var downstreamIface: TestNetworkInterface? = null
+        var tetheringEventCallback: MyTetheringEventCallback? = null
+        var downstreamReader: TapPacketReader? = null
+
+        val discoveryRecord = NsdDiscoveryRecord()
+
+        tryTest {
+            downstreamIface = createTestInterface()
+            val iface = tetheredInterface
+            assertEquals(iface, downstreamIface?.interfaceName)
+            val request = TetheringRequest.Builder(TETHERING_ETHERNET)
+                .setConnectivityScope(CONNECTIVITY_SCOPE_LOCAL).build()
+            tetheringEventCallback = enableEthernetTethering(
+                iface, request,
+                null /* any upstream */
+            ).apply {
+                awaitInterfaceLocalOnly()
+            }
+            // This shouldn't be flaky because the TAP interface will buffer all packets even
+            // before the reader is started.
+            downstreamReader = makePacketReader(downstreamIface)
+            waitForRouterAdvertisement(downstreamReader, iface, WAIT_RA_TIMEOUT_MS)
+
+            nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryRecord)
+            discoveryRecord.expectCallback<NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStarted>()
+            assertNotNull(downstreamReader?.pollForQuery("$serviceType.local", 12 /* type PTR */))
+        } cleanupStep {
+            nsdManager.stopServiceDiscovery(discoveryRecord)
+            discoveryRecord.expectCallback<NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped>()
+        } cleanupStep {
+            maybeStopTapPacketReader(downstreamReader)
+        } cleanupStep {
+            maybeCloseTestInterface(downstreamIface)
+        } cleanup {
+            maybeUnregisterTetheringEventCallback(tetheringEventCallback)
+        }
+    }
+
+    @Test
+    fun testMdnsDiscoveryWorkOnTetheringInterface() {
+        assumeFalse(isInterfaceForTetheringAvailable)
+        setIncludeTestInterfaces(true)
+
+        var downstreamIface: TestNetworkInterface? = null
+        var tetheringEventCallback: MyTetheringEventCallback? = null
+        var downstreamReader: TapPacketReader? = null
+
+        val discoveryRecord = NsdDiscoveryRecord()
+
+        tryTest {
+            downstreamIface = createTestInterface()
+            val iface = tetheredInterface
+            assertEquals(iface, downstreamIface?.interfaceName)
+
+            val localAddr = LinkAddress("192.0.2.3/28")
+            val clientAddr = LinkAddress("192.0.2.2/28")
+            val request = TetheringRequest.Builder(TETHERING_ETHERNET)
+                .setStaticIpv4Addresses(localAddr, clientAddr)
+                .setShouldShowEntitlementUi(false).build()
+            tetheringEventCallback = enableEthernetTethering(
+                iface, request,
+                null /* any upstream */
+            ).apply {
+                awaitInterfaceTethered()
+            }
+
+            val fd = downstreamIface?.fileDescriptor?.fileDescriptor
+            assertNotNull(fd)
+            downstreamReader = makePacketReader(fd, getMTU(downstreamIface))
+
+            nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryRecord)
+            discoveryRecord.expectCallback<NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStarted>()
+            assertNotNull(downstreamReader?.pollForQuery("$serviceType.local", 12 /* type PTR */))
+            // TODO: Add another test to check packet reply can trigger serviceFound.
+        } cleanupStep {
+            nsdManager.stopServiceDiscovery(discoveryRecord)
+            discoveryRecord.expectCallback<NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped>()
+        } cleanupStep {
+            maybeStopTapPacketReader(downstreamReader)
+        } cleanupStep {
+            maybeCloseTestInterface(downstreamIface)
+        } cleanup {
+            maybeUnregisterTetheringEventCallback(tetheringEventCallback)
+        }
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index 17a135a..9c44a3e 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -20,6 +20,7 @@
 import android.app.compat.CompatChanges
 import android.net.ConnectivityManager
 import android.net.ConnectivityManager.NetworkCallback
+import android.net.DnsResolver
 import android.net.InetAddresses.parseNumericAddress
 import android.net.LinkAddress
 import android.net.LinkProperties
@@ -38,36 +39,26 @@
 import android.net.TestNetworkManager
 import android.net.TestNetworkSpecifier
 import android.net.connectivity.ConnectivityCompatChanges
-import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStarted
-import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped
-import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.ServiceFound
-import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.ServiceLost
-import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.StartDiscoveryFailed
-import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.StopDiscoveryFailed
-import android.net.cts.NsdManagerTest.NsdRegistrationRecord.RegistrationEvent.RegistrationFailed
-import android.net.cts.NsdManagerTest.NsdRegistrationRecord.RegistrationEvent.ServiceRegistered
-import android.net.cts.NsdManagerTest.NsdRegistrationRecord.RegistrationEvent.ServiceUnregistered
-import android.net.cts.NsdManagerTest.NsdRegistrationRecord.RegistrationEvent.UnregistrationFailed
-import android.net.cts.NsdManagerTest.NsdResolveRecord.ResolveEvent.ResolutionStopped
-import android.net.cts.NsdManagerTest.NsdResolveRecord.ResolveEvent.ResolveFailed
-import android.net.cts.NsdManagerTest.NsdResolveRecord.ResolveEvent.ServiceResolved
-import android.net.cts.NsdManagerTest.NsdResolveRecord.ResolveEvent.StopResolutionFailed
-import android.net.cts.NsdManagerTest.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.RegisterCallbackFailed
-import android.net.cts.NsdManagerTest.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.ServiceUpdated
-import android.net.cts.NsdManagerTest.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.ServiceUpdatedLost
-import android.net.cts.NsdManagerTest.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.UnregisterCallbackSucceeded
+import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStarted
+import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped
+import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.ServiceFound
+import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.ServiceLost
+import android.net.cts.NsdRegistrationRecord.RegistrationEvent.ServiceRegistered
+import android.net.cts.NsdRegistrationRecord.RegistrationEvent.ServiceUnregistered
+import android.net.cts.NsdResolveRecord.ResolveEvent.ResolutionStopped
+import android.net.cts.NsdResolveRecord.ResolveEvent.ServiceResolved
+import android.net.cts.NsdResolveRecord.ResolveEvent.StopResolutionFailed
+import android.net.cts.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.ServiceUpdated
+import android.net.cts.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.ServiceUpdatedLost
+import android.net.cts.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.UnregisterCallbackSucceeded
 import android.net.cts.util.CtsNetUtils
 import android.net.nsd.NsdManager
-import android.net.nsd.NsdManager.DiscoveryListener
-import android.net.nsd.NsdManager.RegistrationListener
-import android.net.nsd.NsdManager.ResolveListener
 import android.net.nsd.NsdServiceInfo
 import android.net.nsd.OffloadEngine
 import android.net.nsd.OffloadServiceInfo
 import android.os.Build
 import android.os.Handler
 import android.os.HandlerThread
-import android.os.Process.myTid
 import android.platform.test.annotations.AppModeFull
 import android.system.ErrnoException
 import android.system.Os
@@ -84,25 +75,20 @@
 import com.android.compatibility.common.util.PollingCheck
 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
+import com.android.testutils.assertEmpty
 import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk30
 import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk33
 import com.android.testutils.runAsShell
@@ -123,7 +109,6 @@
 import kotlin.test.assertFailsWith
 import kotlin.test.assertNotNull
 import kotlin.test.assertNull
-import kotlin.test.assertTrue
 import kotlin.test.fail
 import org.junit.After
 import org.junit.Assert.assertArrayEquals
@@ -137,7 +122,6 @@
 
 private const val TAG = "NsdManagerTest"
 private const val TIMEOUT_MS = 2000L
-private const val NO_CALLBACK_TIMEOUT_MS = 200L
 // Registration may take a long time if there are devices with the same hostname on the network,
 // as the device needs to try another name and probe again. This is especially true since when using
 // mdnsresponder the usual hostname is "Android", and on conflict "Android-2", "Android-3", ... are
@@ -159,7 +143,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) ?: fail("Could not get NsdManager service")
+    }
 
     private val cm by lazy { context.getSystemService(ConnectivityManager::class.java)!! }
     private val serviceName = "NsdTest%09d".format(Random().nextInt(1_000_000_000))
@@ -185,192 +171,6 @@
         }
     }
 
-    private interface NsdEvent
-    private open class NsdRecord<T : NsdEvent> private constructor(
-        private val history: ArrayTrackRecord<T>,
-        private val expectedThreadId: Int? = null
-    ) : TrackRecord<T> by history {
-        constructor(expectedThreadId: Int? = null) : this(ArrayTrackRecord(), expectedThreadId)
-
-        val nextEvents = history.newReadHead()
-
-        override fun add(e: T): Boolean {
-            if (expectedThreadId != null) {
-                assertEquals(expectedThreadId, myTid(), "Callback is running on the wrong thread")
-            }
-            return history.add(e)
-        }
-
-        inline fun <reified V : NsdEvent> expectCallbackEventually(
-            timeoutMs: Long = TIMEOUT_MS,
-            crossinline predicate: (V) -> Boolean = { true }
-        ): V = nextEvents.poll(timeoutMs) { e -> e is V && predicate(e) } as V?
-                ?: fail("Callback for ${V::class.java.simpleName} not seen after $timeoutMs ms")
-
-        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}")
-            assertTrue(nextEvent is V, "Expected ${V::class.java.simpleName} but got " +
-                    nextEvent.javaClass.simpleName)
-            return nextEvent
-        }
-
-        inline fun assertNoCallback(timeoutMs: Long = NO_CALLBACK_TIMEOUT_MS) {
-            val cb = nextEvents.poll(timeoutMs)
-            assertNull(cb, "Expected no callback but got $cb")
-        }
-    }
-
-    private class NsdRegistrationRecord(expectedThreadId: Int? = null) : RegistrationListener,
-            NsdRecord<NsdRegistrationRecord.RegistrationEvent>(expectedThreadId) {
-        sealed class RegistrationEvent : NsdEvent {
-            abstract val serviceInfo: NsdServiceInfo
-
-            data class RegistrationFailed(
-                override val serviceInfo: NsdServiceInfo,
-                val errorCode: Int
-            ) : RegistrationEvent()
-
-            data class UnregistrationFailed(
-                override val serviceInfo: NsdServiceInfo,
-                val errorCode: Int
-            ) : RegistrationEvent()
-
-            data class ServiceRegistered(override val serviceInfo: NsdServiceInfo) :
-                    RegistrationEvent()
-            data class ServiceUnregistered(override val serviceInfo: NsdServiceInfo) :
-                    RegistrationEvent()
-        }
-
-        override fun onRegistrationFailed(si: NsdServiceInfo, err: Int) {
-            add(RegistrationFailed(si, err))
-        }
-
-        override fun onUnregistrationFailed(si: NsdServiceInfo, err: Int) {
-            add(UnregistrationFailed(si, err))
-        }
-
-        override fun onServiceRegistered(si: NsdServiceInfo) {
-            add(ServiceRegistered(si))
-        }
-
-        override fun onServiceUnregistered(si: NsdServiceInfo) {
-            add(ServiceUnregistered(si))
-        }
-    }
-
-    private class NsdDiscoveryRecord(expectedThreadId: Int? = null) :
-            DiscoveryListener, NsdRecord<NsdDiscoveryRecord.DiscoveryEvent>(expectedThreadId) {
-        sealed class DiscoveryEvent : NsdEvent {
-            data class StartDiscoveryFailed(val serviceType: String, val errorCode: Int) :
-                    DiscoveryEvent()
-
-            data class StopDiscoveryFailed(val serviceType: String, val errorCode: Int) :
-                    DiscoveryEvent()
-
-            data class DiscoveryStarted(val serviceType: String) : DiscoveryEvent()
-            data class DiscoveryStopped(val serviceType: String) : DiscoveryEvent()
-            data class ServiceFound(val serviceInfo: NsdServiceInfo) : DiscoveryEvent()
-            data class ServiceLost(val serviceInfo: NsdServiceInfo) : DiscoveryEvent()
-        }
-
-        override fun onStartDiscoveryFailed(serviceType: String, err: Int) {
-            add(StartDiscoveryFailed(serviceType, err))
-        }
-
-        override fun onStopDiscoveryFailed(serviceType: String, err: Int) {
-            add(StopDiscoveryFailed(serviceType, err))
-        }
-
-        override fun onDiscoveryStarted(serviceType: String) {
-            add(DiscoveryStarted(serviceType))
-        }
-
-        override fun onDiscoveryStopped(serviceType: String) {
-            add(DiscoveryStopped(serviceType))
-        }
-
-        override fun onServiceFound(si: NsdServiceInfo) {
-            add(ServiceFound(si))
-        }
-
-        override fun onServiceLost(si: NsdServiceInfo) {
-            add(ServiceLost(si))
-        }
-
-        fun waitForServiceDiscovered(
-            serviceName: String,
-            serviceType: String,
-            expectedNetwork: Network? = null
-        ): NsdServiceInfo {
-            val serviceFound = expectCallbackEventually<ServiceFound> {
-                it.serviceInfo.serviceName == serviceName &&
-                        (expectedNetwork == null ||
-                                expectedNetwork == it.serviceInfo.network)
-            }.serviceInfo
-            // Discovered service types have a dot at the end
-            assertEquals("$serviceType.", serviceFound.serviceType)
-            return serviceFound
-        }
-    }
-
-    private class NsdResolveRecord : ResolveListener,
-            NsdRecord<NsdResolveRecord.ResolveEvent>() {
-        sealed class ResolveEvent : NsdEvent {
-            data class ResolveFailed(val serviceInfo: NsdServiceInfo, val errorCode: Int) :
-                    ResolveEvent()
-
-            data class ServiceResolved(val serviceInfo: NsdServiceInfo) : ResolveEvent()
-            data class ResolutionStopped(val serviceInfo: NsdServiceInfo) : ResolveEvent()
-            data class StopResolutionFailed(val serviceInfo: NsdServiceInfo, val errorCode: Int) :
-                    ResolveEvent()
-        }
-
-        override fun onResolveFailed(si: NsdServiceInfo, err: Int) {
-            add(ResolveFailed(si, err))
-        }
-
-        override fun onServiceResolved(si: NsdServiceInfo) {
-            add(ServiceResolved(si))
-        }
-
-        override fun onResolutionStopped(si: NsdServiceInfo) {
-            add(ResolutionStopped(si))
-        }
-
-        override fun onStopResolutionFailed(si: NsdServiceInfo, err: Int) {
-            super.onStopResolutionFailed(si, err)
-            add(StopResolutionFailed(si, err))
-        }
-    }
-
-    private class NsdServiceInfoCallbackRecord : NsdManager.ServiceInfoCallback,
-            NsdRecord<NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent>() {
-        sealed class ServiceInfoCallbackEvent : NsdEvent {
-            data class RegisterCallbackFailed(val errorCode: Int) : ServiceInfoCallbackEvent()
-            data class ServiceUpdated(val serviceInfo: NsdServiceInfo) : ServiceInfoCallbackEvent()
-            object ServiceUpdatedLost : ServiceInfoCallbackEvent()
-            object UnregisterCallbackSucceeded : ServiceInfoCallbackEvent()
-        }
-
-        override fun onServiceInfoCallbackRegistrationFailed(err: Int) {
-            add(RegisterCallbackFailed(err))
-        }
-
-        override fun onServiceUpdated(si: NsdServiceInfo) {
-            add(ServiceUpdated(si))
-        }
-
-        override fun onServiceLost() {
-            add(ServiceUpdatedLost)
-        }
-
-        override fun onServiceInfoCallbackUnregistered() {
-            add(UnregisterCallbackSucceeded)
-        }
-    }
-
     private class TestNsdOffloadEngine : OffloadEngine,
         NsdRecord<TestNsdOffloadEngine.OffloadEvent>() {
         sealed class OffloadEvent : NsdEvent {
@@ -626,11 +426,7 @@
 
     @Test
     fun testNsdManager_DiscoverOnNetwork() {
-        val si = NsdServiceInfo()
-        si.serviceType = serviceType
-        si.serviceName = this.serviceName
-        si.port = 12345 // Test won't try to connect so port does not matter
-
+        val si = makeTestServiceInfo()
         val registrationRecord = NsdRegistrationRecord()
         val registeredInfo = registerService(registrationRecord, si)
 
@@ -657,11 +453,7 @@
 
     @Test
     fun testNsdManager_DiscoverWithNetworkRequest() {
-        val si = NsdServiceInfo()
-        si.serviceType = serviceType
-        si.serviceName = this.serviceName
-        si.port = 12345 // Test won't try to connect so port does not matter
-
+        val si = makeTestServiceInfo()
         val handler = Handler(handlerThread.looper)
         val executor = Executor { handler.post(it) }
 
@@ -726,11 +518,6 @@
 
     @Test
     fun testNsdManager_DiscoverWithNetworkRequest_NoMatchingNetwork() {
-        val si = NsdServiceInfo()
-        si.serviceType = serviceType
-        si.serviceName = this.serviceName
-        si.port = 12345 // Test won't try to connect so port does not matter
-
         val handler = Handler(handlerThread.looper)
         val executor = Executor { handler.post(it) }
 
@@ -770,11 +557,7 @@
 
     @Test
     fun testNsdManager_ResolveOnNetwork() {
-        val si = NsdServiceInfo()
-        si.serviceType = serviceType
-        si.serviceName = this.serviceName
-        si.port = 12345 // Test won't try to connect so port does not matter
-
+        val si = makeTestServiceInfo()
         val registrationRecord = NsdRegistrationRecord()
         val registeredInfo = registerService(registrationRecord, si)
         tryTest {
@@ -812,12 +595,7 @@
 
     @Test
     fun testNsdManager_RegisterOnNetwork() {
-        val si = NsdServiceInfo()
-        si.serviceType = serviceType
-        si.serviceName = this.serviceName
-        si.network = testNetwork1.network
-        si.port = 12345 // Test won't try to connect so port does not matter
-
+        val si = makeTestServiceInfo(testNetwork1.network)
         // Register service on testNetwork1
         val registrationRecord = NsdRegistrationRecord()
         registerService(registrationRecord, si)
@@ -1091,11 +869,7 @@
 
     @Test
     fun testStopServiceResolution() {
-        val si = NsdServiceInfo()
-        si.serviceType = this@NsdManagerTest.serviceType
-        si.serviceName = this@NsdManagerTest.serviceName
-        si.port = 12345 // Test won't try to connect so port does not matter
-
+        val si = makeTestServiceInfo()
         val resolveRecord = NsdResolveRecord()
         // Try to resolve an unknown service then stop it immediately.
         // Expected ResolutionStopped callback.
@@ -1113,12 +887,7 @@
         val addresses = lp.addresses
         assertFalse(addresses.isEmpty())
 
-        val si = NsdServiceInfo().apply {
-            serviceType = this@NsdManagerTest.serviceType
-            serviceName = this@NsdManagerTest.serviceName
-            network = testNetwork1.network
-            port = 12345 // Test won't try to connect so port does not matter
-        }
+        val si = makeTestServiceInfo(testNetwork1.network)
 
         // Register service on the network
         val registrationRecord = NsdRegistrationRecord()
@@ -1224,11 +993,7 @@
         // 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 si = makeTestServiceInfo(testNetwork1.network)
 
         val packetReader = TapPacketReader(Handler(handlerThread.looper),
                 testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
@@ -1265,11 +1030,7 @@
         // 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 si = makeTestServiceInfo(testNetwork1.network)
 
         // Register service on testNetwork1
         val registrationRecord = NsdRegistrationRecord()
@@ -1339,6 +1100,127 @@
         }
     }
 
+    // Test that even if only a PTR record is received as a reply when discovering, without the
+    // SRV, TXT, address records as recommended (but not mandated) by RFC 6763 12, the service can
+    // still be discovered.
+    @Test
+    fun testDiscoveryWithPtrOnlyResponse_ServiceIsFound() {
+        // Register service on testNetwork1
+        val discoveryRecord = NsdDiscoveryRecord()
+        val packetReader = TapPacketReader(Handler(handlerThread.looper),
+                testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+        packetReader.startAsyncForTest()
+        handlerThread.waitForIdle(TIMEOUT_MS)
+
+        nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+                testNetwork1.network, { it.run() }, discoveryRecord)
+
+        tryTest {
+            discoveryRecord.expectCallback<DiscoveryStarted>()
+            assertNotNull(packetReader.pollForQuery("$serviceType.local", DnsResolver.TYPE_PTR))
+            /*
+            Generated with:
+            scapy.raw(scapy.DNS(rd=0, qr=1, aa=1, qd = None, an =
+                scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=120,
+                rdata='NsdTest123456789._nmt123456789._tcp.local'))).hex()
+             */
+            val ptrResponsePayload = HexDump.hexStringToByteArray("0000840000000001000000000d5f6e" +
+                    "6d74313233343536373839045f746370056c6f63616c00000c000100000078002b104e736454" +
+                    "6573743132333435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00")
+
+            replaceServiceNameAndTypeWithTestSuffix(ptrResponsePayload)
+            packetReader.sendResponse(buildMdnsPacket(ptrResponsePayload))
+
+            val serviceFound = discoveryRecord.expectCallback<ServiceFound>()
+            serviceFound.serviceInfo.let {
+                assertEquals(serviceName, it.serviceName)
+                // Discovered service types have a dot at the end
+                assertEquals("$serviceType.", it.serviceType)
+                assertEquals(testNetwork1.network, it.network)
+                // ServiceFound does not provide port, address or attributes (only information
+                // available in the PTR record is included in that callback, regardless of whether
+                // other records exist).
+                assertEquals(0, it.port)
+                assertEmpty(it.hostAddresses)
+                assertEquals(0, it.attributes.size)
+            }
+        } cleanup {
+            nsdManager.stopServiceDiscovery(discoveryRecord)
+            discoveryRecord.expectCallback<DiscoveryStopped>()
+        }
+    }
+
+    // Test RFC 6763 12. "Clients MUST be capable of functioning correctly with DNS servers [...]
+    // that fail to generate these additional records automatically, by issuing subsequent queries
+    // for any further record(s) they require"
+    @Test
+    fun testResolveWhenServerSendsNoAdditionalRecord() {
+        // Resolve service on testNetwork1
+        val resolveRecord = NsdResolveRecord()
+        val packetReader = TapPacketReader(Handler(handlerThread.looper),
+                testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+        packetReader.startAsyncForTest()
+        handlerThread.waitForIdle(TIMEOUT_MS)
+
+        val si = makeTestServiceInfo(testNetwork1.network)
+        nsdManager.resolveService(si, { it.run() }, resolveRecord)
+
+        val serviceFullName = "$serviceName.$serviceType.local"
+        // The query should ask for ANY, since both SRV and TXT are requested. Note legacy
+        // mdnsresponder will ask for SRV and TXT separately, and will not proceed to asking for
+        // address records without an answer for both.
+        val srvTxtQuery = packetReader.pollForQuery(serviceFullName, DnsResolver.TYPE_ANY)
+        assertNotNull(srvTxtQuery)
+
+        /*
+        Generated with:
+        scapy.raw(scapy.dns_compress(scapy.DNS(rd=0, qr=1, aa=1, qd = None, an =
+            scapy.DNSRRSRV(rrname='NsdTest123456789._nmt123456789._tcp.local',
+                rclass=0x8001, port=31234, target='testhost.local', ttl=120) /
+            scapy.DNSRR(rrname='NsdTest123456789._nmt123456789._tcp.local', type='TXT', ttl=120,
+                rdata='testkey=testvalue')
+        ))).hex()
+         */
+        val srvTxtResponsePayload = HexDump.hexStringToByteArray("000084000000000200000000104" +
+                "e7364546573743132333435363738390d5f6e6d74313233343536373839045f746370056c6f6" +
+                "3616c0000218001000000780011000000007a020874657374686f7374c030c00c00100001000" +
+                "00078001211746573746b65793d7465737476616c7565")
+        replaceServiceNameAndTypeWithTestSuffix(srvTxtResponsePayload)
+        packetReader.sendResponse(buildMdnsPacket(srvTxtResponsePayload))
+
+        val testHostname = "testhost.local"
+        val addressQuery = packetReader.pollForQuery(testHostname,
+            DnsResolver.TYPE_A, DnsResolver.TYPE_AAAA)
+        assertNotNull(addressQuery)
+
+        /*
+        Generated with:
+        scapy.raw(scapy.dns_compress(scapy.DNS(rd=0, qr=1, aa=1, qd = None, an =
+            scapy.DNSRR(rrname='testhost.local', type='A', ttl=120,
+                rdata='192.0.2.123') /
+            scapy.DNSRR(rrname='testhost.local', type='AAAA', ttl=120,
+                rdata='2001:db8::123')
+        ))).hex()
+         */
+        val addressPayload = HexDump.hexStringToByteArray("0000840000000002000000000874657374" +
+                "686f7374056c6f63616c0000010001000000780004c000027bc00c001c000100000078001020" +
+                "010db8000000000000000000000123")
+        packetReader.sendResponse(buildMdnsPacket(addressPayload))
+
+        val serviceResolved = resolveRecord.expectCallback<ServiceResolved>()
+        serviceResolved.serviceInfo.let {
+            assertEquals(serviceName, it.serviceName)
+            assertEquals(".$serviceType", it.serviceType)
+            assertEquals(testNetwork1.network, it.network)
+            assertEquals(31234, it.port)
+            assertEquals(1, it.attributes.size)
+            assertArrayEquals("testvalue".encodeToByteArray(), it.attributes["testkey"])
+        }
+        assertEquals(
+                setOf(parseNumericAddress("192.0.2.123"), parseNumericAddress("2001:db8::123")),
+                serviceResolved.serviceInfo.hostAddresses.toSet())
+    }
+
     private fun buildConflictingAnnouncement(): ByteBuffer {
         /*
         Generated with:
@@ -1350,21 +1232,37 @@
         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.
+        replaceServiceNameAndTypeWithTestSuffix(mdnsPayload)
+
+        return buildMdnsPacket(mdnsPayload)
+    }
+
+    /**
+     * Replaces occurrences of "NsdTest123456789" and "_nmt123456789" in mDNS payload with the
+     * actual random name and type that are used by the test.
+     */
+    private fun replaceServiceNameAndTypeWithTestSuffix(mdnsPayload: ByteArray) {
         // 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)
+        val packetBuffer = ByteBuffer.wrap(mdnsPayload)
+        replaceAll(packetBuffer, testPacketName, encodedServiceName)
+        replaceAll(packetBuffer, testPacketTypePrefix, encodedTypePrefix)
+    }
+
+    private tailrec fun replaceAll(buffer: ByteBuffer, source: ByteArray, replacement: ByteArray) {
+        assertEquals(source.size, replacement.size)
+        val index = buffer.array().indexOf(source)
+        if (index < 0) return
+
+        val origPosition = buffer.position()
+        buffer.position(index)
+        buffer.put(replacement)
+        buffer.position(origPosition)
+        replaceAll(buffer, source, replacement)
     }
 
     private fun buildMdnsPacket(mdnsPayload: ByteArray): ByteBuffer {
@@ -1414,54 +1312,6 @@
     }
 }
 
-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) {
-    val header: DnsHeader
-        get() = mHeader
-    val records: Array<List<DnsRecord>>
-        get() = mRecords
-
-    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 ->
@@ -1481,3 +1331,8 @@
     }
     return -1
 }
+
+private fun ByteArray?.utf8ToString(): String {
+    if (this == null) return ""
+    return String(this, StandardCharsets.UTF_8)
+}
diff --git a/tests/cts/net/src/android/net/cts/RateLimitTest.java b/tests/cts/net/src/android/net/cts/RateLimitTest.java
index 5c93738..36b98fc 100644
--- a/tests/cts/net/src/android/net/cts/RateLimitTest.java
+++ b/tests/cts/net/src/android/net/cts/RateLimitTest.java
@@ -36,7 +36,6 @@
 import android.icu.text.MessageFormat;
 import android.net.ConnectivityManager;
 import android.net.ConnectivitySettingsManager;
-import android.net.ConnectivityThread;
 import android.net.InetAddresses;
 import android.net.IpPrefix;
 import android.net.LinkAddress;
@@ -190,19 +189,7 @@
             // whatever happens, don't leave the device in rate limited state.
             ConnectivitySettingsManager.setIngressRateLimitInBytesPerSecond(mContext, -1);
         }
-        if (mSocket == null) {
-            // HACK(b/272147742): dump ConnectivityThread if test initialization failed.
-            final StackTraceElement[] elements = ConnectivityThread.get().getStackTrace();
-            final StringBuilder sb = new StringBuilder();
-            // Skip first element as it includes the invocation of getStackTrace()
-            for (int i = 1; i < elements.length; i++) {
-                sb.append(elements[i]);
-                sb.append("\n");
-            }
-            Log.e(TAG, sb.toString());
-        } else {
-            mSocket.close();
-        }
+        if (mSocket != null) mSocket.close();
         if (mNetworkAgent != null) mNetworkAgent.unregister();
         if (mTunInterface != null) mTunInterface.getFileDescriptor().close();
         if (mCm != null) mCm.unregisterNetworkCallback(mNetworkCallback);
diff --git a/tests/cts/net/src/android/net/cts/VpnServiceTest.java b/tests/cts/net/src/android/net/cts/VpnServiceTest.java
index 5c7b5ca..f343e83 100644
--- a/tests/cts/net/src/android/net/cts/VpnServiceTest.java
+++ b/tests/cts/net/src/android/net/cts/VpnServiceTest.java
@@ -15,12 +15,28 @@
  */
 package android.net.cts;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeFalse;
+
+import android.content.Context;
 import android.content.Intent;
+import android.content.pm.PackageManager;
 import android.net.VpnService;
 import android.os.ParcelFileDescriptor;
 import android.platform.test.annotations.AppModeFull;
 import android.test.AndroidTestCase;
 
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.io.File;
 import java.net.DatagramSocket;
 import java.net.Socket;
@@ -30,12 +46,21 @@
  * blocks us from writing tests for positive cases. For now we only test for
  * negative cases, and we will try to cover the rest in the future.
  */
-public class VpnServiceTest extends AndroidTestCase {
+@RunWith(AndroidJUnit4.class)
+public class VpnServiceTest {
 
     private static final String TAG = VpnServiceTest.class.getSimpleName();
 
+    private final Context mContext = InstrumentationRegistry.getContext();
     private VpnService mVpnService = new VpnService();
 
+    @Before
+    public void setUp() {
+        assumeFalse("Skipping test because watches don't support VPN",
+            mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH));
+    }
+
+    @Test
     @AppModeFull(reason = "PackageManager#queryIntentActivities cannot access in instant app mode")
     public void testPrepare() throws Exception {
         // Should never return null since we are not prepared.
@@ -47,6 +72,7 @@
         assertEquals(1, count);
     }
 
+    @Test
     @AppModeFull(reason = "establish() requires prepare(), which requires PackageManager access")
     public void testEstablish() throws Exception {
         ParcelFileDescriptor descriptor = null;
@@ -63,6 +89,7 @@
         }
     }
 
+    @Test
     @AppModeFull(reason = "Protecting sockets requires prepare(), which requires PackageManager")
     public void testProtect_DatagramSocket() throws Exception {
         DatagramSocket socket = new DatagramSocket();
@@ -78,6 +105,7 @@
         }
     }
 
+    @Test
     @AppModeFull(reason = "Protecting sockets requires prepare(), which requires PackageManager")
     public void testProtect_Socket() throws Exception {
         Socket socket = new Socket();
@@ -93,6 +121,7 @@
         }
     }
 
+    @Test
     @AppModeFull(reason = "Protecting sockets requires prepare(), which requires PackageManager")
     public void testProtect_int() throws Exception {
         DatagramSocket socket = new DatagramSocket();
@@ -114,6 +143,7 @@
         }
     }
 
+    @Test
     public void testTunDevice() throws Exception {
         File file = new File("/dev/tun");
         assertTrue(file.exists());
diff --git a/tests/integration/AndroidManifest.xml b/tests/integration/AndroidManifest.xml
index 50f02d3..cea83c7 100644
--- a/tests/integration/AndroidManifest.xml
+++ b/tests/integration/AndroidManifest.xml
@@ -40,6 +40,8 @@
     <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
     <!-- Querying the resources package -->
     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
+    <!-- Register UidFrozenStateChangedCallback -->
+    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
     <application android:debuggable="true">
         <uses-library android:name="android.test.runner"/>
 
diff --git a/tests/integration/util/com/android/server/NetworkAgentWrapper.java b/tests/integration/util/com/android/server/NetworkAgentWrapper.java
index edd201d..ec09f9e 100644
--- a/tests/integration/util/com/android/server/NetworkAgentWrapper.java
+++ b/tests/integration/util/com/android/server/NetworkAgentWrapper.java
@@ -19,6 +19,7 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
+import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
@@ -123,6 +124,10 @@
         mNetworkCapabilities.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED);
         mNetworkCapabilities.addTransportType(transport);
         switch (transport) {
+            case TRANSPORT_BLUETOOTH:
+                // Score for Wear companion proxy network; not BLUETOOTH tethering.
+                mScore = new NetworkScore.Builder().setLegacyInt(100).build();
+                break;
             case TRANSPORT_ETHERNET:
                 mScore = new NetworkScore.Builder().setLegacyInt(70).build();
                 break;
diff --git a/tests/mts/bpf_existence_test.cpp b/tests/mts/bpf_existence_test.cpp
index 15263cc..cff4d6f 100644
--- a/tests/mts/bpf_existence_test.cpp
+++ b/tests/mts/bpf_existence_test.cpp
@@ -40,6 +40,7 @@
 #define TETHERING "/sys/fs/bpf/tethering/"
 #define PRIVATE "/sys/fs/bpf/net_private/"
 #define SHARED "/sys/fs/bpf/net_shared/"
+#define NETD_RO "/sys/fs/bpf/netd_readonly/"
 #define NETD "/sys/fs/bpf/netd_shared/"
 
 class BpfExistenceTest : public ::testing::Test {
@@ -119,9 +120,9 @@
 };
 
 // Provided by *current* mainline module for T+ devices with 5.4+ kernels
-static const set<string> MAINLINE_FOR_T_5_4_PLUS = {
-    SHARED "prog_block_bind4_block_port",
-    SHARED "prog_block_bind6_block_port",
+static const set<string> MAINLINE_FOR_T_4_19_PLUS = {
+    NETD_RO "prog_block_bind4_block_port",
+    NETD_RO "prog_block_bind6_block_port",
 };
 
 // Provided by *current* mainline module for T+ devices with 5.15+ kernels
@@ -129,6 +130,16 @@
     SHARED "prog_dscpPolicy_schedcls_set_dscp_ether",
 };
 
+// Provided by *current* mainline module for U+ devices
+static const set<string> MAINLINE_FOR_U_PLUS = {
+    NETD "map_netd_packet_trace_enabled_map",
+};
+
+// Provided by *current* mainline module for U+ devices with 5.10+ kernels
+static const set<string> MAINLINE_FOR_U_5_10_PLUS = {
+    NETD "map_netd_packet_trace_ringbuf",
+};
+
 static void addAll(set<string>& a, const set<string>& b) {
     a.insert(b.begin(), b.end());
 }
@@ -166,11 +177,13 @@
     // T still only requires Linux Kernel 4.9+.
     DO_EXPECT(IsAtLeastT(), MAINLINE_FOR_T_PLUS);
     DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(4, 14, 0), MAINLINE_FOR_T_4_14_PLUS);
-    DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(5, 4, 0), MAINLINE_FOR_T_5_4_PLUS);
+    DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(4, 19, 0), MAINLINE_FOR_T_4_19_PLUS);
     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));
+    DO_EXPECT(IsAtLeastU(), MAINLINE_FOR_U_PLUS);
+    DO_EXPECT(IsAtLeastU() && isAtLeastKernelVersion(5, 10, 0), MAINLINE_FOR_U_5_10_PLUS);
 
     // V requires Linux Kernel 4.19+, but nothing (as yet) added or removed in V.
     if (IsAtLeastV()) ASSERT_TRUE(isAtLeastKernelVersion(4, 19, 0));
diff --git a/tests/unit/AndroidManifest.xml b/tests/unit/AndroidManifest.xml
index 5d4bdf7..2853f31 100644
--- a/tests/unit/AndroidManifest.xml
+++ b/tests/unit/AndroidManifest.xml
@@ -49,6 +49,9 @@
     <uses-permission android:name="android.permission.NETWORK_FACTORY" />
     <uses-permission android:name="android.permission.NETWORK_STATS_PROVIDER" />
     <uses-permission android:name="android.permission.CONTROL_OEM_PAID_NETWORK_PREFERENCE" />
+    <!-- Workaround for flakes where the launcher package is not found despite the <queries> tag
+         below (b/286550950). -->
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
 
     <!-- Declare the intent that the test intends to query. This is necessary for
          UiDevice.getLauncherPackageName which is used in NetworkNotificationManagerTest
diff --git a/tests/unit/java/android/net/VpnManagerTest.java b/tests/unit/java/android/net/VpnManagerTest.java
index 532081a..2ab4e45 100644
--- a/tests/unit/java/android/net/VpnManagerTest.java
+++ b/tests/unit/java/android/net/VpnManagerTest.java
@@ -19,6 +19,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assume.assumeFalse;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.mock;
@@ -27,11 +28,13 @@
 
 import android.content.ComponentName;
 import android.content.Intent;
+import android.content.pm.PackageManager;
 import android.os.Build;
 import android.test.mock.MockContext;
 import android.util.SparseArray;
 
 import androidx.test.filters.SmallTest;
+import androidx.test.InstrumentationRegistry;
 
 import com.android.internal.net.VpnProfile;
 import com.android.internal.util.MessageUtils;
@@ -47,6 +50,7 @@
 @RunWith(DevSdkIgnoreRunner.class)
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class VpnManagerTest {
+
     private static final String PKG_NAME = "fooPackage";
 
     private static final String SESSION_NAME_STRING = "testSession";
@@ -66,6 +70,9 @@
 
     @Before
     public void setUp() throws Exception {
+        assumeFalse("Skipping test because watches don't support VPN",
+            InstrumentationRegistry.getContext().getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_WATCH));
         mMockService = mock(IVpnManager.class);
         mVpnManager = new VpnManager(mMockContext, mMockService);
     }
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 2fccdcb..16f0c44 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -30,6 +30,7 @@
 import static android.Manifest.permission.NETWORK_SETUP_WIZARD;
 import static android.Manifest.permission.NETWORK_STACK;
 import static android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD;
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
 import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_FROZEN;
 import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_UNFROZEN;
 import static android.app.PendingIntent.FLAG_IMMUTABLE;
@@ -128,6 +129,7 @@
 import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS;
 import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS;
 import static android.net.NetworkCapabilities.REDACT_NONE;
+import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
@@ -152,6 +154,7 @@
 import static android.system.OsConstants.IPPROTO_TCP;
 
 import static com.android.server.ConnectivityService.DELAY_DESTROY_FROZEN_SOCKETS_VERSION;
+import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
 import static com.android.server.ConnectivityService.KEY_DESTROY_FROZEN_SOCKETS_VERSION;
 import static com.android.server.ConnectivityService.MAX_NETWORK_REQUESTS_PER_SYSTEM_UID;
 import static com.android.server.ConnectivityService.PREFERENCE_ORDER_MOBILE_DATA_PREFERERRED;
@@ -250,6 +253,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.ModuleInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
@@ -414,7 +418,6 @@
 import com.android.server.connectivity.UidRangeUtils;
 import com.android.server.connectivity.Vpn;
 import com.android.server.connectivity.VpnProfileStore;
-import com.android.server.net.LockdownVpnTracker;
 import com.android.server.net.NetworkPinner;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
@@ -1496,14 +1499,13 @@
         private int mVpnType = VpnManager.TYPE_VPN_SERVICE;
         private UnderlyingNetworkInfo mUnderlyingNetworkInfo;
 
-        // These ConditionVariables allow tests to wait for LegacyVpnRunner to be stopped/started.
+        // This ConditionVariable allow tests to wait for LegacyVpnRunner to be started.
         // TODO: this scheme is ad-hoc and error-prone because it does not fail if, for example, the
         // test expects two starts in a row, or even if the production code calls start twice in a
         // row. find a better solution. Simply putting a method to create a LegacyVpnRunner into
         // Vpn.Dependencies doesn't work because LegacyVpnRunner is not a static class and has
         // extensive access into the internals of Vpn.
         private ConditionVariable mStartLegacyVpnCv = new ConditionVariable();
-        private ConditionVariable mStopVpnRunnerCv = new ConditionVariable();
 
         public MockVpn(int userId) {
             super(startHandlerThreadAndReturnLooper(), mServiceContext,
@@ -1672,12 +1674,6 @@
         public void expectStartLegacyVpnRunner() {
             assertTrue("startLegacyVpnRunner not called after " + TIMEOUT_MS + " ms",
                     mStartLegacyVpnCv.block(TIMEOUT_MS));
-
-            // startLegacyVpn calls stopVpnRunnerPrivileged, which will open mStopVpnRunnerCv, just
-            // before calling startLegacyVpnRunner. Restore mStopVpnRunnerCv, so the test can expect
-            // that the VpnRunner is stopped and immediately restarted by calling
-            // expectStartLegacyVpnRunner() and expectStopVpnRunnerPrivileged() back-to-back.
-            mStopVpnRunnerCv = new ConditionVariable();
         }
 
         @Override
@@ -1688,12 +1684,6 @@
                 mStartLegacyVpnCv = new ConditionVariable();
             }
             mVpnRunner = null;
-            mStopVpnRunnerCv.open();
-        }
-
-        public void expectStopVpnRunnerPrivileged() {
-            assertTrue("stopVpnRunnerPrivileged not called after " + TIMEOUT_MS + " ms",
-                    mStopVpnRunnerCv.block(TIMEOUT_MS));
         }
 
         @Override
@@ -1903,6 +1893,7 @@
         mServiceContext.setPermission(CONTROL_OEM_PAID_NETWORK_PREFERENCE, PERMISSION_GRANTED);
         mServiceContext.setPermission(PACKET_KEEPALIVE_OFFLOAD, PERMISSION_GRANTED);
         mServiceContext.setPermission(CONNECTIVITY_USE_RESTRICTED_NETWORKS, PERMISSION_GRANTED);
+        mServiceContext.setPermission(READ_DEVICE_CONFIG, PERMISSION_GRANTED);
 
         mAlarmManagerThread = new HandlerThread("TestAlarmManager");
         mAlarmManagerThread.start();
@@ -2058,7 +2049,8 @@
 
         @Override
         public CarrierPrivilegeAuthenticator makeCarrierPrivilegeAuthenticator(
-                @NonNull final Context context, @NonNull final TelephonyManager tm) {
+                @NonNull final Context context,
+                @NonNull final TelephonyManager tm) {
             return mDeps.isAtLeastT() ? mCarrierPrivilegeAuthenticator : null;
         }
 
@@ -2150,6 +2142,7 @@
         public boolean isFeatureEnabled(Context context, String name) {
             switch (name) {
                 case ConnectivityFlags.NO_REMATCH_ALL_REQUESTS_ON_REGISTER:
+                case ConnectivityFlags.CARRIER_SERVICE_CHANGED_USE_CALLBACK:
                     return true;
                 case KEY_DESTROY_FROZEN_SOCKETS_VERSION:
                     return true;
@@ -2404,6 +2397,7 @@
         final String myPackageName = mContext.getPackageName();
         final PackageInfo myPackageInfo = mContext.getPackageManager().getPackageInfo(
                 myPackageName, PackageManager.GET_PERMISSIONS);
+        myPackageInfo.setLongVersionCode(9_999_999L);
         doReturn(new String[] {myPackageName}).when(mPackageManager)
                 .getPackagesForUid(Binder.getCallingUid());
         doReturn(myPackageInfo).when(mPackageManager).getPackageInfoAsUser(
@@ -2415,6 +2409,13 @@
                 buildPackageInfo(/* SYSTEM */ false, VPN_UID)
         })).when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
 
+        final ModuleInfo moduleInfo = new ModuleInfo();
+        moduleInfo.setPackageName(TETHERING_MODULE_NAME);
+        doReturn(moduleInfo).when(mPackageManager)
+                .getModuleInfo(TETHERING_MODULE_NAME, PackageManager.MODULE_APEX_NAME);
+        doReturn(myPackageInfo).when(mPackageManager)
+                .getPackageInfo(TETHERING_MODULE_NAME, PackageManager.MATCH_APEX);
+
         // Create a fake always-on VPN package.
         final int userId = UserHandle.getCallingUserId();
         final ApplicationInfo applicationInfo = new ApplicationInfo();
@@ -4871,6 +4872,34 @@
     }
 
     @Test
+    public void testNoAvoidCaptivePortalOnWearProxy() throws Exception {
+        // Bring up a BLUETOOTH network which is companion proxy on wear
+        // then set captive portal.
+        mockHasSystemFeature(PackageManager.FEATURE_WATCH, true);
+        setCaptivePortalMode(ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_AVOID);
+        TestNetworkAgentWrapper btAgent = new TestNetworkAgentWrapper(TRANSPORT_BLUETOOTH);
+        final String firstRedirectUrl = "http://example.com/firstPath";
+
+        btAgent.connectWithCaptivePortal(firstRedirectUrl, false /* privateDnsProbeSent */);
+        btAgent.assertNotDisconnected(TIMEOUT_MS);
+    }
+
+    @Test
+    public void testAvoidCaptivePortalOnBluetooth() throws Exception {
+        // When not on Wear, BLUETOOTH is just regular network,
+        // then set captive portal.
+        mockHasSystemFeature(PackageManager.FEATURE_WATCH, false);
+        setCaptivePortalMode(ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_AVOID);
+        TestNetworkAgentWrapper btAgent = new TestNetworkAgentWrapper(TRANSPORT_BLUETOOTH);
+        final String firstRedirectUrl = "http://example.com/firstPath";
+
+        btAgent.connectWithCaptivePortal(firstRedirectUrl, false /* privateDnsProbeSent */);
+
+        btAgent.expectDisconnected();
+        btAgent.expectPreventReconnectReceived();
+    }
+
+    @Test
     public void testCaptivePortalApi() throws Exception {
         mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED);
 
@@ -10195,74 +10224,28 @@
         // Pretend lockdown VPN was configured.
         final VpnProfile profile = setupLegacyLockdownVpn();
 
-        // LockdownVpnTracker disables the Vpn teardown code and enables lockdown.
-        // Check the VPN's state before it does so.
-        assertTrue(mMockVpn.getEnableTeardown());
-        assertFalse(mMockVpn.getLockdown());
-
-        // VMSHandlerThread was used inside VpnManagerService and taken into LockDownVpnTracker.
-        // VpnManagerService was decoupled from this test but this handlerThread is still required
-        // in LockDownVpnTracker. Keep it until LockDownVpnTracker related verification is moved to
-        // its own test.
-        final HandlerThread VMSHandlerThread = new HandlerThread("TestVpnManagerService");
-        VMSHandlerThread.start();
-
-        // LockdownVpnTracker is created from VpnManagerService but VpnManagerService is decoupled
-        // from ConnectivityServiceTest. Create it directly to simulate LockdownVpnTracker is
-        // created.
-        // TODO: move LockdownVpnTracker related tests to its own test.
-        // Lockdown VPN disables teardown and enables lockdown.
-        final LockdownVpnTracker lockdownVpnTracker = new LockdownVpnTracker(mServiceContext,
-                VMSHandlerThread.getThreadHandler(), mMockVpn, profile);
-        lockdownVpnTracker.init();
-        assertFalse(mMockVpn.getEnableTeardown());
-        assertTrue(mMockVpn.getLockdown());
+        // Init lockdown state to simulate LockdownVpnTracker behavior.
+        mCm.setLegacyLockdownVpnEnabled(true);
+        mMockVpn.setEnableTeardown(false);
+        mMockVpn.setLockdown(true);
 
         // Bring up a network.
-        // Expect nothing to happen because the network does not have an IPv4 default route: legacy
-        // VPN only supports IPv4.
         final LinkProperties cellLp = new LinkProperties();
         cellLp.setInterfaceName("rmnet0");
-        cellLp.addLinkAddress(new LinkAddress("2001:db8::1/64"));
-        cellLp.addRoute(new RouteInfo(new IpPrefix("::/0"), null, "rmnet0"));
-        mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
-        mCellAgent.connect(false /* validated */);
-        callback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellAgent);
-        defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellAgent);
-        systemDefaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellAgent);
-        waitForIdle();
-        assertNull(mMockVpn.getAgent());
-
-        // Add an IPv4 address. Ideally the VPN should start, but it doesn't because nothing calls
-        // LockdownVpnTracker#handleStateChangedLocked. This is a bug.
-        // TODO: consider fixing this.
         cellLp.addLinkAddress(new LinkAddress("192.0.2.2/25"));
         cellLp.addRoute(new RouteInfo(new IpPrefix("0.0.0.0/0"), null, "rmnet0"));
-        mCellAgent.sendLinkProperties(cellLp);
-        callback.expect(LINK_PROPERTIES_CHANGED, mCellAgent);
-        defaultCallback.expect(LINK_PROPERTIES_CHANGED, mCellAgent);
-        systemDefaultCallback.expect(LINK_PROPERTIES_CHANGED, mCellAgent);
-        waitForIdle();
-        assertNull(mMockVpn.getAgent());
-
-        // Disconnect, then try again with a network that supports IPv4 at connection time.
-        // Expect lockdown VPN to come up.
-        ExpectedBroadcast b1 = expectConnectivityAction(TYPE_MOBILE, DetailedState.DISCONNECTED);
-        mCellAgent.disconnect();
-        callback.expect(LOST, mCellAgent);
-        defaultCallback.expect(LOST, mCellAgent);
-        systemDefaultCallback.expect(LOST, mCellAgent);
-        b1.expectBroadcast();
-
         // When lockdown VPN is active, the NetworkInfo state in CONNECTIVITY_ACTION is overwritten
         // with the state of the VPN network. So expect a CONNECTING broadcast.
-        b1 = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTING);
+        ExpectedBroadcast b1 = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTING);
         mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
         mCellAgent.connect(false /* validated */);
         callback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellAgent);
         defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellAgent);
         systemDefaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellAgent);
         b1.expectBroadcast();
+        // Simulate LockdownVpnTracker attempting to start the VPN since it received the
+        // systemDefault callback.
+        mMockVpn.startLegacyVpnPrivileged(profile, mCellAgent.getNetwork(), cellLp);
         assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
         assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
         assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
@@ -10316,23 +10299,25 @@
         b1 = expectConnectivityAction(TYPE_MOBILE, DetailedState.DISCONNECTED);
         // Wifi is CONNECTING because the VPN isn't up yet.
         b2 = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTING);
-        ExpectedBroadcast b3 = expectConnectivityAction(TYPE_VPN, DetailedState.DISCONNECTED);
         mWiFiAgent.connect(false /* validated */);
+        // Wifi is not blocked since VPN network is still connected.
+        callback.expectAvailableCallbacksUnvalidated(mWiFiAgent);
+        defaultCallback.assertNoCallback();
+        systemDefaultCallback.expectAvailableCallbacksUnvalidated(mWiFiAgent);
         b1.expectBroadcast();
         b2.expectBroadcast();
-        b3.expectBroadcast();
-        mMockVpn.expectStopVpnRunnerPrivileged();
-        mMockVpn.expectStartLegacyVpnRunner();
 
-        // TODO: why is wifi not blocked? Is it because when this callback is sent, the VPN is still
-        // connected, so the network is not considered blocked by the lockdown UID ranges? But the
-        // fact that a VPN is connected should only result in the VPN itself being unblocked, not
-        // any other network. Bug in isUidBlockedByVpn?
-        callback.expectAvailableCallbacksUnvalidated(mWiFiAgent);
+        // Simulate LockdownVpnTracker restarting the VPN since it received the systemDefault
+        // callback with different network.
+        final ExpectedBroadcast b3 = expectConnectivityAction(TYPE_VPN, DetailedState.DISCONNECTED);
+        mMockVpn.stopVpnRunnerPrivileged();
+        mMockVpn.startLegacyVpnPrivileged(profile, mWiFiAgent.getNetwork(), wifiLp);
+        mMockVpn.expectStartLegacyVpnRunner();
         callback.expect(LOST, mMockVpn);
         defaultCallback.expect(LOST, mMockVpn);
         defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mWiFiAgent);
-        systemDefaultCallback.expectAvailableCallbacksUnvalidated(mWiFiAgent);
+        systemDefaultCallback.assertNoCallback();
+        b3.expectBroadcast();
 
         // While the VPN is reconnecting on the new network, everything is blocked.
         assertActiveNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
@@ -10377,15 +10362,22 @@
         b2 = expectConnectivityAction(TYPE_VPN, DetailedState.DISCONNECTED);
         mWiFiAgent.disconnect();
         callback.expect(LOST, mWiFiAgent);
+        callback.expectCaps(mMockVpn, c -> !c.hasTransport(TRANSPORT_WIFI));
+        defaultCallback.expectCaps(mMockVpn, c -> !c.hasTransport(TRANSPORT_WIFI));
+        systemDefaultCallback.expect(LOST, mWiFiAgent);
+        // TODO: There should only be one LOST callback. Since the WIFI network is underlying a VPN
+        // network, ConnectivityService#propagateUnderlyingNetworkCapabilities() causes a rematch to
+        // occur. Notably, this happens before setting the satisfiers of its network requests to
+        // null. Since the satisfiers are set to null in the rematch, an extra LOST callback is
+        // called.
         systemDefaultCallback.expect(LOST, mWiFiAgent);
         b1.expectBroadcast();
-        callback.expectCaps(mMockVpn, c -> !c.hasTransport(TRANSPORT_WIFI));
-        mMockVpn.expectStopVpnRunnerPrivileged();
+        mMockVpn.stopVpnRunnerPrivileged();
         callback.expect(LOST, mMockVpn);
+        defaultCallback.expect(LOST, mMockVpn);
         b2.expectBroadcast();
 
-        VMSHandlerThread.quitSafely();
-        VMSHandlerThread.join();
+        assertNoCallbacks(callback, defaultCallback, systemDefaultCallback);
     }
 
     @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
@@ -10772,6 +10764,8 @@
         final RouteInfo ipv4Subnet = new RouteInfo(myIpv4, null, MOBILE_IFNAME);
         final RouteInfo stackedDefault =
                 new RouteInfo((IpPrefix) null, myIpv4.getAddress(), CLAT_MOBILE_IFNAME);
+        final BaseNetdUnsolicitedEventListener netdUnsolicitedListener =
+                getRegisteredNetdUnsolicitedEventListener();
 
         final NetworkRequest networkRequest = new NetworkRequest.Builder()
                 .addTransportType(TRANSPORT_CELLULAR)
@@ -10839,7 +10833,6 @@
         assertRoutesRemoved(cellNetId, ipv4Subnet);
 
         // When NAT64 prefix discovery succeeds, LinkProperties are updated and clatd is started.
-        Nat464Xlat clat = getNat464Xlat(mCellAgent);
         assertNull(mCm.getLinkProperties(mCellAgent.getNetwork()).getNat64Prefix());
         mService.mResolverUnsolEventCallback.onNat64PrefixEvent(
                 makeNat64PrefixEvent(cellNetId, PREFIX_OPERATION_ADDED, kNat64PrefixString, 96));
@@ -10850,7 +10843,8 @@
         verifyClatdStart(null /* inOrder */, MOBILE_IFNAME, cellNetId, kNat64Prefix.toString());
 
         // Clat iface comes up. Expect stacked link to be added.
-        clat.interfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true);
+        netdUnsolicitedListener.onInterfaceLinkStateChanged(
+                CLAT_MOBILE_IFNAME, true);
         networkCallback.expect(LINK_PROPERTIES_CHANGED, mCellAgent);
         List<LinkProperties> stackedLps = mCm.getLinkProperties(mCellAgent.getNetwork())
                 .getStackedLinks();
@@ -10896,7 +10890,7 @@
                 kOtherNat64Prefix.toString());
         networkCallback.expect(LINK_PROPERTIES_CHANGED, mCellAgent,
                 cb -> cb.getLp().getNat64Prefix().equals(kOtherNat64Prefix));
-        clat.interfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true);
+        netdUnsolicitedListener.onInterfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true);
         networkCallback.expect(LINK_PROPERTIES_CHANGED, mCellAgent,
                 cb -> cb.getLp().getStackedLinks().size() == 1);
         assertRoutesAdded(cellNetId, stackedDefault);
@@ -10924,7 +10918,7 @@
         assertRoutesRemoved(cellNetId, stackedDefault);
 
         // The interface removed callback happens but has no effect after stop is called.
-        clat.interfaceRemoved(CLAT_MOBILE_IFNAME);
+        netdUnsolicitedListener.onInterfaceRemoved(CLAT_MOBILE_IFNAME);
         networkCallback.assertNoCallback();
         verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_MOBILE_IFNAME);
 
@@ -10961,7 +10955,7 @@
         verifyClatdStart(null /* inOrder */, MOBILE_IFNAME, cellNetId, kNat64Prefix.toString());
 
         // Clat iface comes up. Expect stacked link to be added.
-        clat.interfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true);
+        netdUnsolicitedListener.onInterfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true);
         networkCallback.expect(LINK_PROPERTIES_CHANGED, mCellAgent,
                 cb -> cb.getLp().getStackedLinks().size() == 1
                         && cb.getLp().getNat64Prefix() != null);
@@ -11029,8 +11023,7 @@
 
         // Clatd is started and clat iface comes up. Expect stacked link to be added.
         verifyClatdStart(null /* inOrder */, MOBILE_IFNAME, cellNetId, kNat64Prefix.toString());
-        clat = getNat464Xlat(mCellAgent);
-        clat.interfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true /* up */);
+        netdUnsolicitedListener.onInterfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true /* up */);
         networkCallback.expect(LINK_PROPERTIES_CHANGED, mCellAgent,
                 cb -> cb.getLp().getStackedLinks().size() == 1
                         && cb.getLp().getNat64Prefix().equals(kNat64Prefix));
diff --git a/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java b/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
index 3849e49..8113626 100644
--- a/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
@@ -20,12 +20,15 @@
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.telephony.TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED;
 
+import static com.android.server.connectivity.ConnectivityFlags.CARRIER_SERVICE_CHANGED_USE_CALLBACK;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
@@ -34,9 +37,9 @@
 import static org.mockito.Mockito.verify;
 
 import android.annotation.NonNull;
+import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
-import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.net.NetworkCapabilities;
@@ -49,14 +52,17 @@
 import com.android.networkstack.apishim.TelephonyManagerShimImpl;
 import com.android.networkstack.apishim.common.TelephonyManagerShim.CarrierPrivilegesListenerShim;
 import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
+import com.android.server.ConnectivityService;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRunner;
 
-import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 import org.mockito.ArgumentCaptor;
 
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Map;
 
@@ -79,11 +85,13 @@
     @NonNull private TestCarrierPrivilegeAuthenticator mCarrierPrivilegeAuthenticator;
     private final int mCarrierConfigPkgUid = 12345;
     private final String mTestPkg = "com.android.server.connectivity.test";
+    private final BroadcastReceiver mMultiSimBroadcastReceiver;
 
     public class TestCarrierPrivilegeAuthenticator extends CarrierPrivilegeAuthenticator {
         TestCarrierPrivilegeAuthenticator(@NonNull final Context c,
+                @NonNull final ConnectivityService.Dependencies deps,
                 @NonNull final TelephonyManager t) {
-            super(c, t, mTelephonyManagerShim);
+            super(c, deps, t, mTelephonyManagerShim);
         }
         @Override
         protected int getSlotIndex(int subId) {
@@ -92,15 +100,20 @@
         }
     }
 
-    public CarrierPrivilegeAuthenticatorTest() {
+    /** Parameters to test both using callbacks or the old broadcast */
+    @Parameterized.Parameters
+    public static Collection<Boolean> shouldUseCallbacks() {
+        return Arrays.asList(true, false);
+    }
+
+    public CarrierPrivilegeAuthenticatorTest(final boolean useCallbacks) throws Exception {
         mContext = mock(Context.class);
         mTelephonyManager = mock(TelephonyManager.class);
         mTelephonyManagerShim = mock(TelephonyManagerShimImpl.class);
         mPackageManager = mock(PackageManager.class);
-    }
-
-    @Before
-    public void setUp() throws Exception {
+        final ConnectivityService.Dependencies deps = mock(ConnectivityService.Dependencies.class);
+        doReturn(useCallbacks).when(deps).isFeatureEnabled(any() /* context */,
+                eq(CARRIER_SERVICE_CHANGED_USE_CALLBACK));
         doReturn(SUBSCRIPTION_COUNT).when(mTelephonyManager).getActiveModemCount();
         doReturn(mTestPkg).when(mTelephonyManagerShim)
                 .getCarrierServicePackageNameForLogicalSlot(anyInt());
@@ -109,13 +122,13 @@
         applicationInfo.uid = mCarrierConfigPkgUid;
         doReturn(applicationInfo).when(mPackageManager).getApplicationInfo(eq(mTestPkg), anyInt());
         mCarrierPrivilegeAuthenticator =
-                new TestCarrierPrivilegeAuthenticator(mContext, mTelephonyManager);
-    }
-
-    private IntentFilter getIntentFilter() {
-        final ArgumentCaptor<IntentFilter> captor = ArgumentCaptor.forClass(IntentFilter.class);
-        verify(mContext).registerReceiver(any(), captor.capture(), any(), any());
-        return captor.getValue();
+                new TestCarrierPrivilegeAuthenticator(mContext, deps, mTelephonyManager);
+        final ArgumentCaptor<BroadcastReceiver> receiverCaptor =
+                ArgumentCaptor.forClass(BroadcastReceiver.class);
+        verify(mContext).registerReceiver(receiverCaptor.capture(), argThat(filter ->
+                filter.getAction(0).equals(ACTION_MULTI_SIM_CONFIG_CHANGED)
+        ), any() /* broadcast permissions */, any() /* handler */);
+        mMultiSimBroadcastReceiver = receiverCaptor.getValue();
     }
 
     private Map<Integer, CarrierPrivilegesListenerShim> getCarrierPrivilegesListeners() {
@@ -138,15 +151,6 @@
     }
     @Test
     public void testConstructor() throws Exception {
-        verify(mContext).registerReceiver(
-                        eq(mCarrierPrivilegeAuthenticator),
-                        any(IntentFilter.class),
-                        any(),
-                        any());
-        final IntentFilter filter = getIntentFilter();
-        assertEquals(1, filter.countActions());
-        assertTrue(filter.hasAction(ACTION_MULTI_SIM_CONFIG_CHANGED));
-
         // Two listeners originally registered, one for slot 0 and one for slot 1
         final Map<Integer, CarrierPrivilegesListenerShim> initialListeners =
                 getCarrierPrivilegesListeners();
@@ -154,6 +158,8 @@
         assertNotNull(initialListeners.get(1));
         assertEquals(2, initialListeners.size());
 
+        initialListeners.get(0).onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+
         final NetworkCapabilities.Builder ncBuilder = new NetworkCapabilities.Builder()
                 .addTransportType(TRANSPORT_CELLULAR)
                 .setNetworkSpecifier(new TelephonyNetworkSpecifier(0));
@@ -174,8 +180,11 @@
         assertEquals(2, initialListeners.size());
 
         doReturn(1).when(mTelephonyManager).getActiveModemCount();
-        mCarrierPrivilegeAuthenticator.onReceive(
-                mContext, buildTestMultiSimConfigBroadcastIntent());
+
+        // This is a little bit cavalier in that the call to onReceive is not on the handler
+        // thread that was specified in registerReceiver.
+        // TODO : capture the handler and call this on it if this causes flakiness.
+        mMultiSimBroadcastReceiver.onReceive(mContext, buildTestMultiSimConfigBroadcastIntent());
         // Check all listeners have been removed
         for (CarrierPrivilegesListenerShim listener : initialListeners.values()) {
             verify(mTelephonyManagerShim).removeCarrierPrivilegesListener(eq(listener));
@@ -187,6 +196,8 @@
         assertNotNull(newListeners.get(0));
         assertEquals(1, newListeners.size());
 
+        newListeners.get(0).onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+
         final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(0);
         final NetworkCapabilities nc = new NetworkCapabilities.Builder()
                 .addTransportType(TRANSPORT_CELLULAR)
@@ -212,6 +223,7 @@
         applicationInfo.uid = mCarrierConfigPkgUid + 1;
         doReturn(applicationInfo).when(mPackageManager).getApplicationInfo(eq(mTestPkg), anyInt());
         listener.onCarrierPrivilegesChanged(Collections.emptyList(), new int[] {});
+        listener.onCarrierServiceChanged(null, applicationInfo.uid);
 
         assertFalse(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
                 mCarrierConfigPkgUid, nc));
@@ -221,6 +233,9 @@
 
     @Test
     public void testDefaultSubscription() throws Exception {
+        final CarrierPrivilegesListenerShim listener = getCarrierPrivilegesListeners().get(0);
+        listener.onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+
         final NetworkCapabilities.Builder ncBuilder = new NetworkCapabilities.Builder();
         ncBuilder.addTransportType(TRANSPORT_CELLULAR);
         assertFalse(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
diff --git a/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
index 4158663..88044be 100644
--- a/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
@@ -508,10 +508,10 @@
         // Expected mtu is that the detected mtu minus MTU_DELTA(28).
         assertEquals(1372, ClatCoordinator.adjustMtu(1400));
         assertEquals(1472, ClatCoordinator.adjustMtu(ETHER_MTU));
-        assertEquals(65508, ClatCoordinator.adjustMtu(CLAT_MAX_MTU));
+        assertEquals(1500, ClatCoordinator.adjustMtu(CLAT_MAX_MTU));
 
-        // Expected mtu is that CLAT_MAX_MTU(65536) minus MTU_DELTA(28).
-        assertEquals(65508, ClatCoordinator.adjustMtu(CLAT_MAX_MTU + 1 /* over maximum mtu */));
+        // Expected mtu is that CLAT_MAX_MTU(1528) minus MTU_DELTA(28).
+        assertEquals(1500, ClatCoordinator.adjustMtu(CLAT_MAX_MTU + 1 /* over maximum mtu */));
     }
 
     private void verifyDump(final ClatCoordinator coordinator, boolean clatStarted) {
diff --git a/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java b/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java
index 58c0114..2fe8713 100644
--- a/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java
+++ b/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java
@@ -86,7 +86,6 @@
     @Mock ClatCoordinator mClatCoordinator;
 
     TestLooper mLooper;
-    Handler mHandler;
     NetworkAgentConfig mAgentConfig = new NetworkAgentConfig();
 
     Nat464Xlat makeNat464Xlat(boolean isCellular464XlatEnabled) {
@@ -96,6 +95,14 @@
             }
         };
 
+        // The test looper needs to be created here on the test case thread and not in setUp,
+        // because setUp and test cases are run in different threads. Creating the test looper in
+        // setUp would make Looper.getThread() return the setUp thread, which does not match the
+        // test case thread that is actually used to process the messages.
+        mLooper = new TestLooper();
+        final Handler handler = new Handler(mLooper.getLooper());
+        doReturn(handler).when(mNai).handler();
+
         return new Nat464Xlat(mNai, mNetd, mDnsResolver, deps) {
             @Override protected int getNetId() {
                 return NETID;
@@ -117,9 +124,6 @@
 
     @Before
     public void setUp() throws Exception {
-        mLooper = new TestLooper();
-        mHandler = new Handler(mLooper.getLooper());
-
         MockitoAnnotations.initMocks(this);
 
         mNai.linkProperties = new LinkProperties();
@@ -130,7 +134,6 @@
         markNetworkConnected();
         when(mNai.connService()).thenReturn(mConnectivity);
         when(mNai.netAgentConfig()).thenReturn(mAgentConfig);
-        when(mNai.handler()).thenReturn(mHandler);
         final InterfaceConfigurationParcel mConfig = new InterfaceConfigurationParcel();
         when(mNetd.interfaceGetCfg(eq(STACKED_IFACE))).thenReturn(mConfig);
         mConfig.ipv4Addr = ADDR.getAddress().getHostAddress();
@@ -272,8 +275,7 @@
         verifyClatdStart(null /* inOrder */);
 
         // Stacked interface up notification arrives.
-        nat.interfaceLinkStateChanged(STACKED_IFACE, true);
-        mLooper.dispatchNext();
+        nat.handleInterfaceLinkStateChanged(STACKED_IFACE, true);
 
         verify(mNetd).interfaceGetCfg(eq(STACKED_IFACE));
         verify(mConnectivity).handleUpdateLinkProperties(eq(mNai), c.capture());
@@ -294,8 +296,7 @@
         // Verify the generated v6 is reset when clat is stopped.
         assertNull(nat.mIPv6Address);
         // Stacked interface removed notification arrives and is ignored.
-        nat.interfaceRemoved(STACKED_IFACE);
-        mLooper.dispatchNext();
+        nat.handleInterfaceRemoved(STACKED_IFACE);
 
         verifyNoMoreInteractions(mNetd, mConnectivity);
     }
@@ -324,8 +325,7 @@
         verifyClatdStart(inOrder);
 
         // Stacked interface up notification arrives.
-        nat.interfaceLinkStateChanged(STACKED_IFACE, true);
-        mLooper.dispatchNext();
+        nat.handleInterfaceLinkStateChanged(STACKED_IFACE, true);
 
         inOrder.verify(mConnectivity).handleUpdateLinkProperties(eq(mNai), c.capture());
         assertFalse(c.getValue().getStackedLinks().isEmpty());
@@ -344,10 +344,8 @@
 
         if (interfaceRemovedFirst) {
             // Stacked interface removed notification arrives and is ignored.
-            nat.interfaceRemoved(STACKED_IFACE);
-            mLooper.dispatchNext();
-            nat.interfaceLinkStateChanged(STACKED_IFACE, false);
-            mLooper.dispatchNext();
+            nat.handleInterfaceRemoved(STACKED_IFACE);
+            nat.handleInterfaceLinkStateChanged(STACKED_IFACE, false);
         }
 
         assertTrue(c.getValue().getStackedLinks().isEmpty());
@@ -361,15 +359,12 @@
 
         if (!interfaceRemovedFirst) {
             // Stacked interface removed notification arrives and is ignored.
-            nat.interfaceRemoved(STACKED_IFACE);
-            mLooper.dispatchNext();
-            nat.interfaceLinkStateChanged(STACKED_IFACE, false);
-            mLooper.dispatchNext();
+            nat.handleInterfaceRemoved(STACKED_IFACE);
+            nat.handleInterfaceLinkStateChanged(STACKED_IFACE, false);
         }
 
         // Stacked interface up notification arrives.
-        nat.interfaceLinkStateChanged(STACKED_IFACE, true);
-        mLooper.dispatchNext();
+        nat.handleInterfaceLinkStateChanged(STACKED_IFACE, true);
 
         inOrder.verify(mConnectivity).handleUpdateLinkProperties(eq(mNai), c.capture());
         assertFalse(c.getValue().getStackedLinks().isEmpty());
@@ -411,8 +406,7 @@
         verifyClatdStart(null /* inOrder */);
 
         // Stacked interface up notification arrives.
-        nat.interfaceLinkStateChanged(STACKED_IFACE, true);
-        mLooper.dispatchNext();
+        nat.handleInterfaceLinkStateChanged(STACKED_IFACE, true);
 
         verify(mNetd).interfaceGetCfg(eq(STACKED_IFACE));
         verify(mConnectivity, times(1)).handleUpdateLinkProperties(eq(mNai), c.capture());
@@ -421,8 +415,7 @@
         assertRunning(nat);
 
         // Stacked interface removed notification arrives (clatd crashed, ...).
-        nat.interfaceRemoved(STACKED_IFACE);
-        mLooper.dispatchNext();
+        nat.handleInterfaceRemoved(STACKED_IFACE);
 
         verifyClatdStop(null /* inOrder */);
         verify(mConnectivity, times(2)).handleUpdateLinkProperties(eq(mNai), c.capture());
@@ -457,12 +450,10 @@
         assertIdle(nat);
 
         // In-flight interface up notification arrives: no-op
-        nat.interfaceLinkStateChanged(STACKED_IFACE, true);
-        mLooper.dispatchNext();
+        nat.handleInterfaceLinkStateChanged(STACKED_IFACE, true);
 
         // Interface removed notification arrives after stopClatd() takes effect: no-op.
-        nat.interfaceRemoved(STACKED_IFACE);
-        mLooper.dispatchNext();
+        nat.handleInterfaceRemoved(STACKED_IFACE);
 
         assertIdle(nat);
 
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt b/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt
index 1e3f389..87f7369 100644
--- a/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt
@@ -18,9 +18,12 @@
 
 import android.net.NetworkCapabilities
 import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL as NET_CAP_PORTAL
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET as NET_CAP_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_PRIORITIZE_BANDWIDTH as NET_CAP_PRIO_BW
 import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
 import android.net.NetworkCapabilities.TRANSPORT_WIFI
 import android.net.NetworkScore.KEEP_CONNECTED_NONE
+import android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY
 import android.net.NetworkScore.POLICY_EXITING as EXITING
 import android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY as PRIMARY
 import android.net.NetworkScore.POLICY_YIELD_TO_BAD_WIFI as YIELD_TO_BAD_WIFI
@@ -50,8 +53,8 @@
 class NetworkRankerTest(private val activelyPreferBadWifi: Boolean) {
     private val mRanker = NetworkRanker(NetworkRanker.Configuration(activelyPreferBadWifi))
 
-    private class TestScore(private val sc: FullScore, private val nc: NetworkCapabilities)
-            : NetworkRanker.Scoreable {
+    private class TestScore(private val sc: FullScore, private val nc: NetworkCapabilities) :
+            NetworkRanker.Scoreable {
         override fun getScore() = sc
         override fun getCapsNoCopy(): NetworkCapabilities = nc
     }
@@ -196,4 +199,41 @@
         val badExitingWifi = TestScore(score(EVER_EVALUATED, EVER_VALIDATED, EXITING), CAPS_WIFI)
         assertEquals(cell, rank(cell, badExitingWifi))
     }
+
+    @Test
+    fun testValidatedPolicyStrongerThanSlice() {
+        val unvalidatedNonslice = TestScore(score(EVER_EVALUATED),
+                caps(TRANSPORT_CELLULAR, NET_CAP_INTERNET))
+        val slice = TestScore(score(EVER_EVALUATED, IS_VALIDATED),
+                caps(TRANSPORT_CELLULAR, NET_CAP_INTERNET, NET_CAP_PRIO_BW))
+        assertEquals(slice, rank(slice, unvalidatedNonslice))
+    }
+
+    @Test
+    fun testPrimaryPolicyStrongerThanSlice() {
+        val nonslice = TestScore(score(EVER_EVALUATED),
+                caps(TRANSPORT_CELLULAR, NET_CAP_INTERNET))
+        val primarySlice = TestScore(score(EVER_EVALUATED, POLICY_TRANSPORT_PRIMARY),
+                caps(TRANSPORT_CELLULAR, NET_CAP_INTERNET, NET_CAP_PRIO_BW))
+        assertEquals(primarySlice, rank(nonslice, primarySlice))
+    }
+
+    @Test
+    fun testPreferNonSlices() {
+        // Slices lose to non-slices for general ranking
+        val nonslice = TestScore(score(EVER_EVALUATED, IS_VALIDATED),
+                caps(TRANSPORT_CELLULAR, NET_CAP_INTERNET))
+        val slice = TestScore(score(EVER_EVALUATED, IS_VALIDATED),
+                caps(TRANSPORT_CELLULAR, NET_CAP_INTERNET, NET_CAP_PRIO_BW))
+        assertEquals(nonslice, rank(slice, nonslice))
+    }
+
+    @Test
+    fun testSlicePolicyStrongerThanTransport() {
+        val nonSliceCell = TestScore(score(EVER_EVALUATED, IS_VALIDATED),
+                caps(TRANSPORT_CELLULAR, NET_CAP_INTERNET))
+        val sliceWifi = TestScore(score(EVER_EVALUATED, IS_VALIDATED),
+                caps(TRANSPORT_WIFI, NET_CAP_INTERNET, NET_CAP_PRIO_BW))
+        assertEquals(nonSliceCell, rank(nonSliceCell, sliceWifi))
+    }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index 56346ad..d674767 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -1839,6 +1839,22 @@
         // a subsequent CL.
     }
 
+    @Test
+    public void testStartLegacyVpnIpv6() throws Exception {
+        setMockedUsers(PRIMARY_USER);
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
+        final LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName(EGRESS_IFACE);
+        lp.addLinkAddress(new LinkAddress("2001:db8::1/64"));
+        final RouteInfo defaultRoute = new RouteInfo(
+                new IpPrefix(Inet6Address.ANY, 0), null, EGRESS_IFACE);
+        lp.addRoute(defaultRoute);
+
+        // IllegalStateException thrown since legacy VPN only supports IPv4.
+        assertThrows(IllegalStateException.class,
+                () -> vpn.startLegacyVpn(mVpnProfile, EGRESS_NETWORK, lp));
+    }
+
     private Vpn startLegacyVpn(final Vpn vpn, final VpnProfile vpnProfile) throws Exception {
         setMockedUsers(PRIMARY_USER);
 
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt
index c39ee1e..2797462 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt
@@ -82,7 +82,8 @@
 
     @Test
     fun testAnnounce() {
-        val replySender = MdnsReplySender( thread.looper, socket, buffer, sharedLog)
+        val replySender = MdnsReplySender(
+                thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */)
         @Suppress("UNCHECKED_CAST")
         val cb = mock(MdnsPacketRepeater.PacketRepeaterCallback::class.java)
                 as MdnsPacketRepeater.PacketRepeaterCallback<BaseAnnouncementInfo>
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
index c19747e..a67dc5e 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
@@ -190,8 +190,8 @@
     fun testReplyToQuery() {
         addServiceAndFinishProbing(TEST_SERVICE_ID_1, TEST_SERVICE_1)
 
-        val mockReply = mock(MdnsRecordRepository.ReplyInfo::class.java)
-        doReturn(mockReply).`when`(repository).getReply(any(), any())
+        val testReply = MdnsReplyInfo(emptyList(), emptyList(), 0, InetSocketAddress(0))
+        doReturn(testReply).`when`(repository).getReply(any(), any())
 
         // Query obtained with:
         // scapy.raw(scapy.DNS(
@@ -216,7 +216,7 @@
             assertContentEquals(arrayOf("_testservice", "_tcp", "local"), it.questions[0].name)
         }
 
-        verify(replySender).queueReply(mockReply)
+        verify(replySender).queueReply(testReply)
     }
 
     @Test
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
index 3701b0c..8917ed3 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
@@ -42,6 +42,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;
@@ -69,6 +70,7 @@
     @Mock private SocketCreationCallback mSocketCreationCallback;
     @Mock private SharedLog mSharedLog;
     private MdnsMultinetworkSocketClient mSocketClient;
+    private HandlerThread mHandlerThread;
     private Handler mHandler;
     private SocketKey mSocketKey;
 
@@ -76,14 +78,23 @@
     public void setUp() throws SocketException {
         MockitoAnnotations.initMocks(this);
 
-        final HandlerThread thread = new HandlerThread("MdnsMultinetworkSocketClientTest");
-        thread.start();
-        mHandler = new Handler(thread.getLooper());
+        mHandlerThread = new HandlerThread("MdnsMultinetworkSocketClientTest");
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
         mSocketKey = new SocketKey(1000 /* interfaceIndex */);
-        mSocketClient = new MdnsMultinetworkSocketClient(thread.getLooper(), mProvider, mSharedLog);
+        mSocketClient = new MdnsMultinetworkSocketClient(
+                mHandlerThread.getLooper(), mProvider, mSharedLog);
         mHandler.post(() -> mSocketClient.setCallback(mCallback));
     }
 
+    @After
+    public void tearDown() throws Exception {
+        if (mHandlerThread != null) {
+            mHandlerThread.quitSafely();
+            mHandlerThread.join();
+        }
+    }
+
     private SocketCallback expectSocketCallback() {
         return expectSocketCallback(mListener, mNetwork);
     }
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 b667e5f..28ea4b6 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketTest.kt
@@ -32,7 +32,7 @@
         // Probe packet with 1 question for Android.local, and 4 additionalRecords with 4 addresses
         // for Android.local (similar to legacy mdnsresponder probes, although it used to put 4
         // identical questions(!!) for Android.local when there were 4 addresses).
-        val packetHex = "00000000000100000004000007416e64726f6964056c6f63616c0000ff0001c00c000100" +
+        val packetHex = "007b0000000100000004000007416e64726f6964056c6f63616c0000ff0001c00c000100" +
                 "01000000780004c000027bc00c001c000100000078001020010db8000000000000000000000123c0" +
                 "0c001c000100000078001020010db8000000000000000000000456c00c001c000100000078001020" +
                 "010db8000000000000000000000789"
@@ -41,6 +41,7 @@
         val reader = MdnsPacketReader(bytes, bytes.size)
         val packet = MdnsPacket.parse(reader)
 
+        assertEquals(123, packet.transactionId)
         assertEquals(1, packet.questions.size)
         assertEquals(0, packet.answers.size)
         assertEquals(4, packet.authorityRecords.size)
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
index f284819..5b7c0ba 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
@@ -119,7 +119,8 @@
 
     @Test
     fun testProbe() {
-        val replySender = MdnsReplySender(thread.looper, socket, buffer, sharedLog)
+        val replySender = MdnsReplySender(
+                thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */)
         val prober = TestProber(thread.looper, replySender, cb, sharedLog)
         val probeInfo = TestProbeInfo(
                 listOf(makeServiceRecord(TEST_SERVICE_NAME_1, 37890)))
@@ -143,7 +144,8 @@
 
     @Test
     fun testProbeMultipleRecords() {
-        val replySender = MdnsReplySender(thread.looper, socket, buffer, sharedLog)
+        val replySender = MdnsReplySender(
+                thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */)
         val prober = TestProber(thread.looper, replySender, cb, sharedLog)
         val probeInfo = TestProbeInfo(listOf(
                 makeServiceRecord(TEST_SERVICE_NAME_1, 37890),
@@ -181,7 +183,8 @@
 
     @Test
     fun testStopProbing() {
-        val replySender = MdnsReplySender(thread.looper, socket, buffer, sharedLog)
+        val replySender = MdnsReplySender(
+                thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */)
         val prober = TestProber(thread.looper, replySender, cb, sharedLog)
         val probeInfo = TestProbeInfo(
                 listOf(makeServiceRecord(TEST_SERVICE_NAME_1, 37890)),
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
index 88fb66a..c9b502e 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -105,6 +105,7 @@
         assertEquals(TEST_SERVICE_ID_1, probingInfo.serviceId)
         val packet = probingInfo.getPacket(0)
 
+        assertEquals(0, packet.transactionId)
         assertEquals(MdnsConstants.FLAGS_QUERY, packet.flags)
         assertEquals(0, packet.answers.size)
         assertEquals(0, packet.additionalRecords.size)
@@ -173,6 +174,7 @@
         assertEquals(1, repository.servicesCount)
         val packet = exitAnnouncement.getPacket(0)
 
+        assertEquals(0, packet.transactionId)
         assertEquals(0x8400 /* response, authoritative */, packet.flags)
         assertEquals(0, packet.questions.size)
         assertEquals(0, packet.authorityRecords.size)
@@ -202,6 +204,7 @@
         assertEquals(1, repository.servicesCount)
         val packet = exitAnnouncement.getPacket(0)
 
+        assertEquals(0, packet.transactionId)
         assertEquals(0x8400 /* response, authoritative */, packet.flags)
         assertEquals(0, packet.questions.size)
         assertEquals(0, packet.authorityRecords.size)
@@ -249,6 +252,7 @@
         repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
         val packet = announcementInfo.getPacket(0)
 
+        assertEquals(0, packet.transactionId)
         assertEquals(0x8400 /* response, authoritative */, packet.flags)
         assertEquals(0, packet.questions.size)
         assertEquals(0, packet.authorityRecords.size)
@@ -372,6 +376,7 @@
         val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
         val serviceType = arrayOf("_testservice", "_tcp", "local")
         val offloadPacket = repository.getOffloadPacket(TEST_SERVICE_ID_1)
+        assertEquals(0, offloadPacket.transactionId)
         assertEquals(0x8400, offloadPacket.flags)
         assertEquals(0, offloadPacket.questions.size)
         assertEquals(0, offloadPacket.additionalRecords.size)
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java
index d71bea4..3fc656a 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java
@@ -17,10 +17,8 @@
 package com.android.server.connectivity.mdns;
 
 import static android.net.InetAddresses.parseNumericAddress;
-
 import static com.android.server.connectivity.mdns.util.MdnsUtils.Clock;
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
-
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -343,9 +341,9 @@
         assertNotNull(parsedPacket);
 
         final Network network = mock(Network.class);
-        responses = decoder.augmentResponses(parsedPacket,
+        responses = new ArraySet<>(decoder.augmentResponses(parsedPacket,
                 /* existingResponses= */ Collections.emptyList(),
-                /* interfaceIndex= */ 10, network /* expireOnExit= */).first;
+                /* interfaceIndex= */ 10, network /* expireOnExit= */).first);
 
         assertEquals(responses.size(), 1);
         assertEquals(responses.valueAt(0).getInterfaceIndex(), 10);
@@ -641,8 +639,8 @@
         final MdnsPacket parsedPacket = MdnsResponseDecoder.parseResponse(data, data.length);
         assertNotNull(parsedPacket);
 
-        return decoder.augmentResponses(parsedPacket,
+        return new ArraySet<>(decoder.augmentResponses(parsedPacket,
                 existingResponses,
-                MdnsSocket.INTERFACE_INDEX_UNSPECIFIED, mock(Network.class)).first;
+                MdnsSocket.INTERFACE_INDEX_UNSPECIFIED, mock(Network.class)).first);
     }
 }
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt
index 6f8ba6c..58f20a9 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt
@@ -1,3 +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.
+ */
+
 @file:Suppress("DEPRECATION") // This file tests a bunch of deprecated methods : don't warn about it
 
 package com.android.server
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt
new file mode 100644
index 0000000..a9b9896
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt
@@ -0,0 +1,58 @@
+/*
+ * 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
+
+import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.net.NetworkScore.KEEP_CONNECTED_FOR_TEST
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class CSKeepConnectedTest : CSTest() {
+    @Test
+    fun testKeepConnectedForTest() {
+        val keepAgent = Agent(score = FromS(NetworkScore.Builder()
+                .setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST)
+                .build()))
+        val dontKeepAgent = Agent()
+        doTestKeepConnected(keepAgent, dontKeepAgent)
+    }
+
+    fun doTestKeepConnected(keepAgent: CSAgentWrapper, dontKeepAgent: CSAgentWrapper) {
+        val cb = TestableNetworkCallback()
+        cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities().build(), cb)
+
+        keepAgent.connect()
+        dontKeepAgent.connect()
+
+        cb.expectAvailableCallbacks(keepAgent.network, validated = false)
+        cb.expectAvailableCallbacks(dontKeepAgent.network, validated = false)
+
+        // After the nascent timer, the agent without keep connected gets lost.
+        cb.expect<Lost>(dontKeepAgent.network)
+        cb.assertNoCallback()
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
index 5ae9232..afaf79f 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
@@ -1,3 +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 com.android.server
 
 import android.content.Context
@@ -19,6 +35,7 @@
 import android.os.HandlerThread
 import com.android.modules.utils.build.SdkLevel
 import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
 import com.android.testutils.TestableNetworkCallback
 import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers.any
@@ -31,6 +48,8 @@
 import kotlin.test.assertEquals
 import kotlin.test.fail
 
+const val SHORT_TIMEOUT_MS = 200L
+
 private inline fun <reified T> ArgumentCaptor() = ArgumentCaptor.forClass(T::class.java)
 
 private val agentCounter = AtomicInteger(1)
@@ -44,6 +63,7 @@
  */
 class CSAgentWrapper(
         val context: Context,
+        val deps: ConnectivityService.Dependencies,
         csHandlerThread: HandlerThread,
         networkStack: NetworkStackClientBase,
         nac: NetworkAgentConfig,
@@ -109,9 +129,10 @@
 
     fun connect() {
         val mgr = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
-        val request = NetworkRequest.Builder().clearCapabilities()
-                .addTransportType(nc.transportTypes[0])
-                .build()
+        val request = NetworkRequest.Builder().apply {
+            clearCapabilities()
+            if (nc.transportTypes.isNotEmpty()) addTransportType(nc.transportTypes[0])
+        }.build()
         val cb = TestableNetworkCallback()
         mgr.registerNetworkCallback(request, cb)
         agent.markConnected()
@@ -131,4 +152,17 @@
         }
         mgr.unregisterNetworkCallback(cb)
     }
+
+    fun disconnect() {
+        val mgr = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+        val request = NetworkRequest.Builder().apply {
+            clearCapabilities()
+            if (nc.transportTypes.isNotEmpty()) addTransportType(nc.transportTypes[0])
+        }.build()
+        val cb = TestableNetworkCallback(timeoutMs = SHORT_TIMEOUT_MS)
+        mgr.registerNetworkCallback(request, cb)
+        cb.eventuallyExpect<Available> { it.network == agent.network }
+        agent.unregister()
+        cb.eventuallyExpect<Lost> { it.network == agent.network }
+    }
 }
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
index 68613a6..2f78212 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -1,3 +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 com.android.server
 
 import android.content.BroadcastReceiver
@@ -10,27 +26,31 @@
 import android.net.ConnectivityManager
 import android.net.INetd
 import android.net.InetAddresses
-import android.net.IpPrefix
-import android.net.LinkAddress
 import android.net.LinkProperties
 import android.net.NetworkAgentConfig
 import android.net.NetworkCapabilities
-import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
-import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_TEST
+import android.net.NetworkCapabilities.TRANSPORT_VPN
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
 import android.net.NetworkPolicyManager
 import android.net.NetworkProvider
 import android.net.NetworkScore
 import android.net.PacProxyManager
-import android.net.RouteInfo
 import android.net.networkstack.NetworkStackClientBase
+import android.os.BatteryStatsManager
 import android.os.Handler
 import android.os.HandlerThread
 import android.os.UserHandle
 import android.os.UserManager
 import android.telephony.TelephonyManager
 import android.testing.TestableContext
+import android.util.ArraySet
 import androidx.test.platform.app.InstrumentationRegistry
+import com.android.internal.app.IBatteryStats
 import com.android.internal.util.test.BroadcastInterceptingContext
 import com.android.modules.utils.build.SdkLevel
 import com.android.networkstack.apishim.common.UnsupportedApiLevelException
@@ -41,6 +61,7 @@
 import com.android.server.connectivity.MultinetworkPolicyTracker
 import com.android.server.connectivity.MultinetworkPolicyTrackerTestDependencies
 import com.android.server.connectivity.ProxyTracker
+import com.android.testutils.visibleOnHandlerThread
 import com.android.testutils.waitForIdle
 import org.mockito.AdditionalAnswers.delegatesTo
 import org.mockito.Mockito.doAnswer
@@ -56,6 +77,25 @@
 
 open class FromS<Type>(val value: Type)
 
+internal const val VERSION_UNMOCKED = -1
+internal const val VERSION_R = 1
+internal const val VERSION_S = 2
+internal const val VERSION_T = 3
+internal const val VERSION_U = 4
+internal const val VERSION_V = 5
+internal const val VERSION_MAX = VERSION_V
+
+private fun NetworkCapabilities.getLegacyType() =
+        when (transportTypes.getOrElse(0) { TRANSPORT_WIFI }) {
+            TRANSPORT_BLUETOOTH -> ConnectivityManager.TYPE_BLUETOOTH
+            TRANSPORT_CELLULAR -> ConnectivityManager.TYPE_MOBILE
+            TRANSPORT_ETHERNET -> ConnectivityManager.TYPE_ETHERNET
+            TRANSPORT_TEST -> ConnectivityManager.TYPE_TEST
+            TRANSPORT_VPN -> ConnectivityManager.TYPE_VPN
+            TRANSPORT_WIFI -> ConnectivityManager.TYPE_WIFI
+            else -> ConnectivityManager.TYPE_NONE
+        }
+
 /**
  * Base class for tests testing ConnectivityService and its satellites.
  *
@@ -71,7 +111,7 @@
     init {
         if (!SdkLevel.isAtLeastS()) {
             throw UnsupportedApiLevelException("CSTest subclasses must be annotated to only " +
-                    "run on S+, e.g. @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)");
+                    "run on S+, e.g. @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)")
         }
     }
 
@@ -104,20 +144,22 @@
     val networkStack = mock<NetworkStackClientBase>()
     val csHandlerThread = HandlerThread("CSTestHandler")
     val sysResources = mock<Resources>().also { initMockedResources(it) }
-    val packageManager = makeMockPackageManager()
+    val packageManager = makeMockPackageManager(instrumentationContext)
     val connResources = makeMockConnResources(sysResources, packageManager)
 
+    val netd = mock<INetd>()
     val bpfNetMaps = mock<BpfNetMaps>()
     val clatCoordinator = mock<ClatCoordinator>()
     val proxyTracker = ProxyTracker(context, mock<Handler>(), 16 /* EVENT_PROXY_HAS_CHANGED */)
     val alarmManager = makeMockAlarmManager()
     val systemConfigManager = makeMockSystemConfigManager()
+    val batteryManager = BatteryStatsManager(mock<IBatteryStats>())
     val telephonyManager = mock<TelephonyManager>().also {
         doReturn(true).`when`(it).isDataCapable()
     }
 
     val deps = CSDeps()
-    val service = makeConnectivityService(context, deps).also { it.systemReadyInternal() }
+    val service = makeConnectivityService(context, netd, deps).also { it.systemReadyInternal() }
     val cm = ConnectivityManager(context, service)
     val csHandler = Handler(csHandlerThread.looper)
 
@@ -130,8 +172,10 @@
         override fun makeHandlerThread() = csHandlerThread
         override fun makeProxyTracker(context: Context, connServiceHandler: Handler) = proxyTracker
 
-        override fun makeCarrierPrivilegeAuthenticator(context: Context, tm: TelephonyManager) =
-                if (SdkLevel.isAtLeastT()) mock<CarrierPrivilegeAuthenticator>() else null
+        override fun makeCarrierPrivilegeAuthenticator(
+                context: Context,
+                tm: TelephonyManager
+        ) = if (SdkLevel.isAtLeastT()) mock<CarrierPrivilegeAuthenticator>() else null
 
         private inner class AOOKTDeps(c: Context) : AutomaticOnOffKeepaliveTracker.Dependencies(c) {
             override fun isTetheringFeatureNotChickenedOut(name: String): Boolean {
@@ -150,6 +194,43 @@
         // checking permissions.
         override fun isFeatureEnabled(context: Context?, name: String?) =
                 enabledFeatures[name] ?: fail("Unmocked feature $name, see CSTest.enabledFeatures")
+
+        // Mocked change IDs
+        private val enabledChangeIds = ArraySet<Long>()
+        fun setChangeIdEnabled(enabled: Boolean, changeId: Long) {
+            // enabledChangeIds is read on the handler thread and maybe the test thread, so
+            // make sure both threads see it before continuing.
+            visibleOnHandlerThread(csHandler) {
+                if (enabled) {
+                    enabledChangeIds.add(changeId)
+                } else {
+                    enabledChangeIds.remove(changeId)
+                }
+            }
+        }
+
+        override fun isChangeEnabled(changeId: Long, pkg: String, user: UserHandle) =
+                changeId in enabledChangeIds
+        override fun isChangeEnabled(changeId: Long, uid: Int) =
+                changeId in enabledChangeIds
+
+        // In AOSP, build version codes can't always distinguish between some versions (e.g. at the
+        // time of this writing U == V). Define custom ones.
+        private var sdkLevel = VERSION_UNMOCKED
+        private val isSdkUnmocked get() = sdkLevel == VERSION_UNMOCKED
+
+        fun setBuildSdk(sdkLevel: Int) {
+            require(sdkLevel <= VERSION_MAX) {
+                "setBuildSdk must not be called with Build.VERSION constants but " +
+                        "CsTest.VERSION_* constants"
+            }
+            visibleOnHandlerThread(csHandler) { this.sdkLevel = sdkLevel }
+        }
+
+        override fun isAtLeastS() = if (isSdkUnmocked) super.isAtLeastS() else sdkLevel >= VERSION_S
+        override fun isAtLeastT() = if (isSdkUnmocked) super.isAtLeastT() else sdkLevel >= VERSION_T
+        override fun isAtLeastU() = if (isSdkUnmocked) super.isAtLeastU() else sdkLevel >= VERSION_U
+        override fun isAtLeastV() = if (isSdkUnmocked) super.isAtLeastV() else sdkLevel >= VERSION_V
     }
 
     inner class CSContext(base: Context) : BroadcastInterceptingContext(base) {
@@ -196,6 +277,7 @@
             Context.ACTIVITY_SERVICE -> activityManager
             Context.SYSTEM_CONFIG_SERVICE -> systemConfigManager
             Context.TELEPHONY_SERVICE -> telephonyManager
+            Context.BATTERY_STATS_SERVICE -> batteryManager
             Context.STATS_MANAGER -> null // Stats manager is final and can't be mocked
             else -> super.getSystemService(serviceName)
         }
@@ -204,28 +286,15 @@
     // Utility methods for subclasses to use
     fun waitForIdle() = csHandlerThread.waitForIdle(HANDLER_TIMEOUT_MS)
 
-    private fun emptyAgentConfig() = NetworkAgentConfig.Builder().build()
-    private fun defaultNc() = NetworkCapabilities.Builder()
-            // Add sensible defaults for agents that don't want to care
-            .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
-            .addCapability(NET_CAPABILITY_NOT_ROAMING)
-            .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
-            .build()
-    private fun defaultScore() = FromS<NetworkScore>(NetworkScore.Builder().build())
-    private fun defaultLp() = LinkProperties().apply {
-        addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
-        addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
-    }
-
     // Network agents. See CSAgentWrapper. This class contains utility methods to simplify
     // creation.
     fun Agent(
-            nac: NetworkAgentConfig = emptyAgentConfig(),
             nc: NetworkCapabilities = defaultNc(),
+            nac: NetworkAgentConfig = emptyAgentConfig(nc.getLegacyType()),
             lp: LinkProperties = defaultLp(),
             score: FromS<NetworkScore> = defaultScore(),
             provider: NetworkProvider? = null
-    ) = CSAgentWrapper(context, csHandlerThread, networkStack, nac, nc, lp, score, provider)
+    ) = CSAgentWrapper(context, deps, csHandlerThread, networkStack, nac, nc, lp, score, provider)
 
     fun Agent(vararg transports: Int, lp: LinkProperties = defaultLp()): CSAgentWrapper {
         val nc = NetworkCapabilities.Builder().apply {
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
index b8f2151..c1828b2 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
@@ -1,3 +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.
+ */
+
 @file:JvmName("CsTestHelpers")
 
 package com.android.server
@@ -14,7 +30,15 @@
 import android.content.res.Resources
 import android.net.IDnsResolver
 import android.net.INetd
+import android.net.IpPrefix
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.NetworkAgentConfig
+import android.net.NetworkCapabilities
+import android.net.NetworkScore
+import android.net.RouteInfo
 import android.net.metrics.IpConnectivityLog
+import android.os.Binder
 import android.os.Handler
 import android.os.HandlerThread
 import android.os.SystemClock
@@ -31,19 +55,38 @@
 import com.android.server.connectivity.ConnectivityResources
 import org.mockito.ArgumentMatchers
 import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.ArgumentMatchers.anyLong
 import org.mockito.ArgumentMatchers.anyString
 import org.mockito.ArgumentMatchers.argThat
 import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mockito
 import org.mockito.Mockito.doAnswer
-import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
 import kotlin.test.fail
 
 internal inline fun <reified T> mock() = Mockito.mock(T::class.java)
 internal inline fun <reified T> any() = any(T::class.java)
 
+internal fun emptyAgentConfig(legacyType: Int) = NetworkAgentConfig.Builder()
+        .setLegacyType(legacyType)
+        .build()
+
+internal fun defaultNc() = NetworkCapabilities.Builder()
+        // Add sensible defaults for agents that don't want to care
+        .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
+        .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
+        .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
+        .build()
+
+internal fun defaultScore() = FromS(NetworkScore.Builder().build())
+
+internal fun defaultLp() = LinkProperties().apply {
+    addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
+    addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
+}
+
 internal fun makeMockContentResolver(context: Context) = MockContentResolver(context).apply {
     addProvider(Settings.AUTHORITY, FakeSettingsProvider())
 }
@@ -59,9 +102,22 @@
     }
 }
 
-internal fun makeMockPackageManager() = mock<PackageManager>().also { pm ->
+internal fun makeMockPackageManager(realContext: Context) = mock<PackageManager>().also { pm ->
     val supported = listOf(FEATURE_WIFI, FEATURE_WIFI_DIRECT, FEATURE_BLUETOOTH, FEATURE_ETHERNET)
     doReturn(true).`when`(pm).hasSystemFeature(argThat { supported.contains(it) })
+    val myPackageName = realContext.packageName
+    val myPackageInfo = realContext.packageManager.getPackageInfo(myPackageName,
+            PackageManager.GET_PERMISSIONS)
+    // Very high version code so that the checks for the module version will always
+    // say that it is recent enough. This is the most sensible default, but if some
+    // test needs to test with different version codes they can re-mock this with a
+    // different value.
+    myPackageInfo.longVersionCode = 9999999L
+    doReturn(arrayOf(myPackageName)).`when`(pm).getPackagesForUid(Binder.getCallingUid())
+    doReturn(myPackageInfo).`when`(pm).getPackageInfoAsUser(
+            eq(myPackageName), anyInt(), eq(UserHandle.getCallingUserId()))
+    doReturn(listOf(myPackageInfo)).`when`(pm)
+            .getInstalledPackagesAsUser(eq(PackageManager.GET_PERMISSIONS), anyInt())
 }
 
 internal fun makeMockConnResources(resources: Resources, pm: PackageManager) = mock<Context>().let {
@@ -129,12 +185,13 @@
 
 private val TEST_LINGER_DELAY_MS = 400
 private val TEST_NASCENT_DELAY_MS = 300
-internal fun makeConnectivityService(context: Context, deps: Dependencies) = ConnectivityService(
-        context,
-        mock<IDnsResolver>(),
-        mock<IpConnectivityLog>(),
-        mock<INetd>(),
-        deps).also {
-    it.mLingerDelayMs = TEST_LINGER_DELAY_MS
-    it.mNascentDelayMs = TEST_NASCENT_DELAY_MS
-}
+internal fun makeConnectivityService(context: Context, netd: INetd, deps: Dependencies) =
+        ConnectivityService(
+                context,
+                mock<IDnsResolver>(),
+                mock<IpConnectivityLog>(),
+                netd,
+                deps).also {
+            it.mLingerDelayMs = TEST_LINGER_DELAY_MS
+            it.mNascentDelayMs = TEST_NASCENT_DELAY_MS
+        }
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
index 292f77e..c477b2c 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
@@ -59,6 +59,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;
@@ -139,6 +140,14 @@
         mUsageCallback = new TestableUsageCallback(mUsageCallbackBinder);
     }
 
+    @After
+    public void tearDown() throws Exception {
+        if (mObserverHandlerThread != null) {
+            mObserverHandlerThread.quitSafely();
+            mObserverHandlerThread.join();
+        }
+    }
+
     @Test
     public void testRegister_thresholdTooLow_setsDefaultThreshold() throws Exception {
         final long thresholdTooLowBytes = 1L;
diff --git a/thread/TEST_MAPPING b/thread/TEST_MAPPING
new file mode 100644
index 0000000..17a74f6
--- /dev/null
+++ b/thread/TEST_MAPPING
@@ -0,0 +1,9 @@
+{
+  // TODO (b/297729075): graduate this test to presubmit once it meets the SLO requirements.
+  // See go/test-mapping-slo-guide
+  "postsubmit": [
+    {
+      "name": "CtsThreadNetworkTestCases"
+    }
+  ]
+}
diff --git a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
new file mode 100644
index 0000000..0219beb
--- /dev/null
+++ b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
@@ -0,0 +1,25 @@
+/**
+ * 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.net.thread;
+
+/**
+* Interface for communicating with ThreadNetworkControllerService.
+* @hide
+*/
+interface IThreadNetworkController {
+    int getThreadVersion();
+}
diff --git a/thread/framework/java/android/net/thread/IThreadNetworkManager.aidl b/thread/framework/java/android/net/thread/IThreadNetworkManager.aidl
new file mode 100644
index 0000000..0e394b1
--- /dev/null
+++ b/thread/framework/java/android/net/thread/IThreadNetworkManager.aidl
@@ -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 android.net.thread;
+
+import android.net.thread.IThreadNetworkController;
+
+/**
+* Interface for communicating with ThreadNetworkService.
+* @hide
+*/
+interface IThreadNetworkManager {
+    List<IThreadNetworkController> getAllThreadNetworkControllers();
+}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
new file mode 100644
index 0000000..fe189c2
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -0,0 +1,62 @@
+/*
+ * 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.net.thread;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.RemoteException;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Provides the primary API for controlling all aspects of a Thread network.
+ *
+ * @hide
+ */
+@SystemApi
+public class ThreadNetworkController {
+
+    /** Thread standard version 1.3. */
+    public static final int THREAD_VERSION_1_3 = 4;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({THREAD_VERSION_1_3})
+    public @interface ThreadVersion {}
+
+    private final IThreadNetworkController mControllerService;
+
+    ThreadNetworkController(@NonNull IThreadNetworkController controllerService) {
+        requireNonNull(controllerService, "controllerService cannot be null");
+
+        mControllerService = controllerService;
+    }
+
+    /** Returns the Thread version this device is operating on. */
+    @ThreadVersion
+    public int getThreadVersion() {
+        try {
+            return mControllerService.getThreadVersion();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkManager.java b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
new file mode 100644
index 0000000..2a253a1
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
@@ -0,0 +1,107 @@
+/*
+ * 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.net.thread;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.os.RemoteException;
+
+import com.android.net.module.util.CollectionUtils;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Provides the primary API for managing app aspects of Thread network connectivity.
+ *
+ * @hide
+ */
+@SystemApi
+@SystemService(ThreadNetworkManager.SERVICE_NAME)
+public class ThreadNetworkManager {
+    /**
+     * This value tracks {@link Context#THREAD_NETWORK_SERVICE}.
+     *
+     * <p>This is needed because at the time this service is created, it needs to support both
+     * Android U and V but {@link Context#THREAD_NETWORK_SERVICE} Is only available on the V branch.
+     *
+     * <p>Note that this is not added to NetworkStack ConstantsShim because we need this constant in
+     * the framework library while ConstantsShim is only linked against the service library.
+     *
+     * @hide
+     */
+    public static final String SERVICE_NAME = "thread_network";
+
+    /**
+     * This value tracks {@link PackageManager#FEATURE_THREAD_NETWORK}.
+     *
+     * <p>This is needed because at the time this service is created, it needs to support both
+     * Android U and V but {@link PackageManager#FEATURE_THREAD_NETWORK} Is only available on the V
+     * branch.
+     *
+     * <p>Note that this is not added to NetworkStack COnstantsShim because we need this constant in
+     * the framework library while ConstantsShim is only linked against the service library.
+     *
+     * @hide
+     */
+    public static final String FEATURE_NAME = "android.hardware.thread_network";
+
+    @NonNull private final Context mContext;
+    @NonNull private final List<ThreadNetworkController> mUnmodifiableControllerServices;
+
+    /**
+     * Creates a new ThreadNetworkManager instance.
+     *
+     * @hide
+     */
+    public ThreadNetworkManager(
+            @NonNull Context context, @NonNull IThreadNetworkManager managerService) {
+        this(context, makeControllers(managerService));
+    }
+
+    private static List<ThreadNetworkController> makeControllers(
+            @NonNull IThreadNetworkManager managerService) {
+        requireNonNull(managerService, "managerService cannot be null");
+
+        List<IThreadNetworkController> controllerServices;
+
+        try {
+            controllerServices = managerService.getAllThreadNetworkControllers();
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+            return Collections.emptyList();
+        }
+
+        return CollectionUtils.map(controllerServices, ThreadNetworkController::new);
+    }
+
+    private ThreadNetworkManager(
+            @NonNull Context context, @NonNull List<ThreadNetworkController> controllerServices) {
+        mContext = context;
+        mUnmodifiableControllerServices = Collections.unmodifiableList(controllerServices);
+    }
+
+    /** Returns the {@link ThreadNetworkController} object of all Thread networks. */
+    @NonNull
+    public List<ThreadNetworkController> getAllThreadNetworkControllers() {
+        return mUnmodifiableControllerServices;
+    }
+}
diff --git a/thread/service/Android.bp b/thread/service/Android.bp
index fda206a..f1af653 100644
--- a/thread/service/Android.bp
+++ b/thread/service/Android.bp
@@ -32,5 +32,11 @@
     // (service-connectivity is only used on 31+) and use 31 here
     min_sdk_version: "30",
     srcs: [":service-thread-sources"],
+    libs: [
+        "framework-connectivity-t-pre-jarjar",
+    ],
+    static_libs: [
+        "net-utils-device-common",
+    ],
     apex_available: ["com.android.tethering"],
 }
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
new file mode 100644
index 0000000..e8b95bc
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -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 com.android.server.thread;
+
+import static android.net.thread.ThreadNetworkController.THREAD_VERSION_1_3;
+
+import android.net.thread.IThreadNetworkController;
+import android.net.thread.ThreadNetworkController;
+
+/** Implementation of the {@link ThreadNetworkController} API. */
+public final class ThreadNetworkControllerService extends IThreadNetworkController.Stub {
+
+    @Override
+    public int getThreadVersion() {
+        return THREAD_VERSION_1_3;
+    }
+}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkService.java b/thread/service/java/com/android/server/thread/ThreadNetworkService.java
new file mode 100644
index 0000000..c6d47df
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkService.java
@@ -0,0 +1,59 @@
+/*
+ * 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.thread;
+
+import android.content.Context;
+import android.net.thread.IThreadNetworkController;
+import android.net.thread.IThreadNetworkManager;
+
+import com.android.server.SystemService;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Implementation of the Thread network service. This is the entry point of Android Thread feature.
+ */
+public class ThreadNetworkService extends IThreadNetworkManager.Stub {
+    private final ThreadNetworkControllerService mControllerService;
+
+    /** Creates a new {@link ThreadNetworkService} object. */
+    public ThreadNetworkService(Context context) {
+        this(context, new ThreadNetworkControllerService());
+    }
+
+    private ThreadNetworkService(
+            Context context, ThreadNetworkControllerService controllerService) {
+        mControllerService = controllerService;
+    }
+
+    /**
+     * Called by the service initializer.
+     *
+     * @see com.android.server.SystemService#onBootPhase
+     */
+    public void onBootPhase(int phase) {
+        if (phase == SystemService.PHASE_BOOT_COMPLETED) {
+            // TODO: initialize ThreadNetworkManagerService
+        }
+    }
+
+    @Override
+    public List<IThreadNetworkController> getAllThreadNetworkControllers() {
+        return Collections.singletonList(mControllerService);
+    }
+}
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
new file mode 100644
index 0000000..278798e
--- /dev/null
+++ b/thread/tests/cts/Android.bp
@@ -0,0 +1,50 @@
+//
+// 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"],
+}
+
+android_test {
+    name: "CtsThreadNetworkTestCases",
+    defaults: ["cts_defaults"],
+    min_sdk_version: "33",
+    sdk_version: "test_current",
+    manifest: "AndroidManifest.xml",
+    test_config: "AndroidTest.xml",
+    srcs: [
+        "src/**/*.java",
+    ],
+    test_suites: [
+        "cts",
+        "general-tests",
+        "mts-tethering",
+    ],
+    static_libs: [
+        "androidx.test.ext.junit",
+        "compatibility-device-util-axt",
+        "ctstestrunner-axt",
+        "net-tests-utils",
+        "truth",
+    ],
+    libs: [
+        "android.test.base",
+        "android.test.runner",
+    ],
+    // Test coverage system runs on different devices. Need to
+    // compile for all architectures.
+    compile_multilib: "both",
+}
diff --git a/thread/tests/cts/AndroidManifest.xml b/thread/tests/cts/AndroidManifest.xml
new file mode 100644
index 0000000..4370fe3
--- /dev/null
+++ b/thread/tests/cts/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    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.
+ -->
+
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.net.thread.cts">
+
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.net.thread.cts"
+        android:label="CTS tests for android.net.thread" />
+</manifest>
diff --git a/thread/tests/cts/AndroidTest.xml b/thread/tests/cts/AndroidTest.xml
new file mode 100644
index 0000000..5ba605f
--- /dev/null
+++ b/thread/tests/cts/AndroidTest.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    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.
+ -->
+
+<configuration description="Config for Thread network CTS test cases">
+    <option name="test-tag" value="CtsThreadNetworkTestCases" />
+    <option name="test-suite-tag" value="cts" />
+    <option name="config-descriptor:metadata" key="component" value="framework" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+    <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+
+    <!--
+        Only run tests if the device under test is SDK version 33 (Android 13) or above.
+        The Thread feature is only available on V+ and U+ TV devices but this test module
+        needs run on T+ because there are testcases which verifies that Thread service
+        is not support on T or T-.
+    -->
+    <object type="module_controller"
+            class="com.android.tradefed.testtype.suite.module.Sdk33ModuleController" />
+
+    <!-- Run tests in MTS only if the Tethering Mainline module is installed. -->
+    <object type="module_controller"
+            class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="mainline-module-package-name" value="com.google.android.tethering" />
+    </object>
+
+    <!-- Install test -->
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="test-file-name" value="CtsThreadNetworkTestCases.apk" />
+        <option name="check-min-sdk" value="true" />
+        <option name="cleanup-apks" value="true" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.net.thread.cts" />
+    </test>
+</configuration>
diff --git a/thread/tests/cts/OWNERS b/thread/tests/cts/OWNERS
new file mode 100644
index 0000000..6065bf8
--- /dev/null
+++ b/thread/tests/cts/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 1203089
+
+include platform/packages/modules/Connectivity:main:/thread/OWNERS
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
new file mode 100644
index 0000000..b3118f4
--- /dev/null
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.net.thread.cts;
+
+import static android.net.thread.ThreadNetworkController.THREAD_VERSION_1_3;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeNotNull;
+
+import android.content.Context;
+import android.net.thread.ThreadNetworkController;
+import android.net.thread.ThreadNetworkManager;
+import android.os.Build;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+/** CTS tests for {@link ThreadNetworkController}. */
+@SmallTest
+@RunWith(DevSdkIgnoreRunner.class)
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) // Thread is available on only U+
+public class ThreadNetworkControllerTest {
+    @Rule public DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private ThreadNetworkManager mManager;
+
+    @Before
+    public void setUp() {
+        mManager = mContext.getSystemService(ThreadNetworkManager.class);
+
+        // TODO: we will also need it in tearDown(), it's better to have a Rule to skip
+        // tests if a feature is not available.
+        assumeNotNull(mManager);
+    }
+
+    private List<ThreadNetworkController> getAllControllers() {
+        return mManager.getAllThreadNetworkControllers();
+    }
+
+    @Test
+    public void getThreadVersion_returnsAtLeastThreadVersion1P3() {
+        for (ThreadNetworkController controller : getAllControllers()) {
+            assertThat(controller.getThreadVersion()).isAtLeast(THREAD_VERSION_1_3);
+        }
+    }
+}
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkManagerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkManagerTest.java
new file mode 100644
index 0000000..b6d0d31
--- /dev/null
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkManagerTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.net.thread.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeNotNull;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.thread.ThreadNetworkController;
+import android.net.thread.ThreadNetworkManager;
+import android.os.Build;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+/** Tests for {@link ThreadNetworkManager}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ThreadNetworkManagerTest {
+    @Rule public DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private final PackageManager mPackageManager = mContext.getPackageManager();
+
+    private ThreadNetworkManager mManager;
+
+    @Before
+    public void setUp() {
+        mManager = mContext.getSystemService(ThreadNetworkManager.class);
+    }
+
+    @Test
+    @IgnoreAfter(Build.VERSION_CODES.TIRAMISU)
+    public void getManager_onTOrLower_returnsNull() {
+        assertThat(mManager).isNull();
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public void getManager_hasThreadFeatureOnVOrHigher_returnsNonNull() {
+        assumeTrue(mPackageManager.hasSystemFeature("android.hardware.thread_network"));
+
+        assertThat(mManager).isNotNull();
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    @IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public void getManager_onUButNotTv_returnsNull() {
+        assumeFalse(mPackageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK));
+
+        assertThat(mManager).isNull();
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    @IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public void getManager_onUAndTv_returnsNonNull() {
+        assumeTrue(mPackageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK));
+
+        assertThat(mManager).isNotNull();
+    }
+
+    @Test
+    public void getManager_noThreadFeature_returnsNull() {
+        assumeFalse(mPackageManager.hasSystemFeature("android.hardware.thread_network"));
+
+        assertThat(mManager).isNull();
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    public void getAllThreadNetworkControllers_managerIsNotNull_returnsNotEmptyList() {
+        assumeNotNull(mManager);
+
+        List<ThreadNetworkController> controllers = mManager.getAllThreadNetworkControllers();
+
+        assertThat(controllers).isNotEmpty();
+    }
+}