Merge "Move tether_utils into utils folder" into main
diff --git a/OWNERS_core_networking_xts b/OWNERS_core_networking_xts
index b24e3ac..9e4e4a1 100644
--- a/OWNERS_core_networking_xts
+++ b/OWNERS_core_networking_xts
@@ -1,11 +1,12 @@
 lorenzo@google.com
 satk@google.com #{LAST_RESORT_SUGGESTION}
 
-# For cherry-picks of CLs that are already merged in aosp/master, or flaky test fixes.
+# For cherry-picks of CLs that are already merged in aosp/master, flaky test
+# fixes, or no-op refactors.
 jchalard@google.com #{LAST_RESORT_SUGGESTION}
-# In addition to cherry-picks and flaky test fixes, also for APF firmware tests
-# (to verify correct behaviour of the wifi APF interpreter)
+# In addition to cherry-picks, flaky test fixes and no-op refactors, also for
+# APF firmware tests (to verify correct behaviour of the wifi APF interpreter)
 maze@google.com #{LAST_RESORT_SUGGESTION}
-# In addition to cherry-picks and flaky test fixes, also for incremental changes on NsdManager tests
-# to increase coverage for existing behavior, and testing of bug fixes in NsdManager
+# In addition to cherry-picks, flaky test fixes and no-op refactors, also for
+# NsdManager tests
 reminv@google.com #{LAST_RESORT_SUGGESTION}
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 304a6ed..8a5d249 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -101,6 +101,7 @@
         "block.o",
         "clatd.o",
         "dscpPolicy.o",
+        "gentle.o",
         "netd.o",
         "offload.o",
         "offload@mainline.o",
@@ -112,7 +113,7 @@
     ],
     prebuilts: [
         "current_sdkinfo",
-        "netbpfload.mainline.rc",
+        "netbpfload.33rc",
         "netbpfload.35rc",
         "ot-daemon.init.34rc",
     ],
diff --git a/Tethering/common/TetheringLib/api/system-current.txt b/Tethering/common/TetheringLib/api/system-current.txt
index a287b42..cccafd5 100644
--- a/Tethering/common/TetheringLib/api/system-current.txt
+++ b/Tethering/common/TetheringLib/api/system-current.txt
@@ -47,6 +47,7 @@
     field public static final int TETHERING_INVALID = -1; // 0xffffffff
     field public static final int TETHERING_NCM = 4; // 0x4
     field public static final int TETHERING_USB = 1; // 0x1
+    field @FlaggedApi("com.android.net.flags.tethering_request_virtual") public static final int TETHERING_VIRTUAL = 7; // 0x7
     field public static final int TETHERING_WIFI = 0; // 0x0
     field public static final int TETHERING_WIFI_P2P = 3; // 0x3
     field public static final int TETHER_ERROR_DHCPSERVER_ERROR = 12; // 0xc
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index 7b769d4..2963f87 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -68,6 +68,8 @@
     public static class Flags {
         static final String TETHERING_REQUEST_WITH_SOFT_AP_CONFIG =
                 "com.android.net.flags.tethering_request_with_soft_ap_config";
+        static final String TETHERING_REQUEST_VIRTUAL =
+                "com.android.net.flags.tethering_request_virtual";
     }
 
     private static final String TAG = TetheringManager.class.getSimpleName();
@@ -195,10 +197,18 @@
     public static final int TETHERING_WIGIG = 6;
 
     /**
+     * VIRTUAL tethering type.
+     * @hide
+     */
+    @FlaggedApi(Flags.TETHERING_REQUEST_VIRTUAL)
+    @SystemApi
+    public static final int TETHERING_VIRTUAL = 7;
+
+    /**
      * The int value of last tethering type.
      * @hide
      */
-    public static final int MAX_TETHERING_TYPE = TETHERING_WIGIG;
+    public static final int MAX_TETHERING_TYPE = TETHERING_VIRTUAL;
 
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
diff --git a/Tethering/res/values-mcc310-mnc004-eu/strings.xml b/Tethering/res/values-mcc310-mnc004-eu/strings.xml
index c970dd7..ff2a505 100644
--- a/Tethering/res/values-mcc310-mnc004-eu/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-eu/strings.xml
@@ -18,7 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="no_upstream_notification_title" msgid="3584617491053416666">"Konexioa partekatzeko aukerak ez du Interneteko konexiorik"</string>
     <string name="no_upstream_notification_message" msgid="5626323795587558017">"Ezin dira konektatu gailuak"</string>
-    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Desaktibatu konexioa partekatzeko aukera"</string>
+    <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Desaktibatu konexioa partekatzea"</string>
     <string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Wifi-gunea edo konexioa partekatzeko aukera aktibatuta dago"</string>
     <string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Baliteke tarifa gehigarriak ordaindu behar izatea ibiltaritza erabili bitartean"</string>
 </resources>
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index d85d92f..29ced23 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -38,6 +38,7 @@
 import static android.net.TetheringManager.TETHERING_INVALID;
 import static android.net.TetheringManager.TETHERING_NCM;
 import static android.net.TetheringManager.TETHERING_USB;
+import static android.net.TetheringManager.TETHERING_VIRTUAL;
 import static android.net.TetheringManager.TETHERING_WIFI;
 import static android.net.TetheringManager.TETHERING_WIFI_P2P;
 import static android.net.TetheringManager.TETHERING_WIGIG;
@@ -278,6 +279,7 @@
     private TetheredInterfaceRequestShim mBluetoothIfaceRequest;
     private String mConfiguredEthernetIface;
     private String mConfiguredBluetoothIface;
+    private String mConfiguredVirtualIface;
     private EthernetCallback mEthernetCallback;
     private TetheredInterfaceCallbackShim mBluetoothCallback;
     private SettingsObserver mSettingsObserver;
@@ -719,6 +721,9 @@
             case TETHERING_ETHERNET:
                 result = setEthernetTethering(enable);
                 break;
+            case TETHERING_VIRTUAL:
+                result = setVirtualMachineTethering(enable);
+                break;
             default:
                 Log.w(TAG, "Invalid tether type.");
                 result = TETHER_ERROR_UNKNOWN_TYPE;
@@ -972,6 +977,21 @@
         }
     }
 
+    private int setVirtualMachineTethering(final boolean enable) {
+        // TODO(340377643): Use bridge ifname when it's introduced, not fixed TAP ifname.
+        if (enable) {
+            mConfiguredVirtualIface = "avf_tap_fixed";
+            enableIpServing(
+                    TETHERING_VIRTUAL,
+                    mConfiguredVirtualIface,
+                    getRequestedState(TETHERING_VIRTUAL));
+        } else if (mConfiguredVirtualIface != null) {
+            ensureIpServerStopped(mConfiguredVirtualIface);
+            mConfiguredVirtualIface = null;
+        }
+        return TETHER_ERROR_NO_ERROR;
+    }
+
     void tether(String iface, int requestedState, final IIntResultListener listener) {
         mHandler.post(() -> {
             try {
diff --git a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
index 120b871..5f04a98 100644
--- a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
+++ b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
@@ -42,7 +42,6 @@
 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.Assert.fail;
 import static org.junit.Assume.assumeFalse;
@@ -105,7 +104,7 @@
     // Used to check if any tethering interface is available. Choose 200ms to be request timeout
     // because the average interface requested time on cuttlefish@acloud is around 10ms.
     // See TetheredInterfaceRequester.getInterface, isInterfaceForTetheringAvailable.
-    private static final int AVAILABLE_TETHER_IFACE_REQUEST_TIMEOUT_MS = 200;
+    private static final int SHORT_TIMEOUT_MS = 1000;
     private static final int TETHER_REACHABILITY_ATTEMPTS = 20;
     protected static final long WAIT_RA_TIMEOUT_MS = 2000;
 
@@ -154,7 +153,7 @@
     private boolean mRunTests;
     private HandlerThread mHandlerThread;
     private Handler mHandler;
-    private TetheredInterfaceRequester mTetheredInterfaceRequester;
+    protected TetheredInterfaceRequester mTetheredInterfaceRequester;
 
     // Late initialization in initTetheringTester().
     private TapPacketReader mUpstreamReader;
@@ -245,12 +244,14 @@
         maybeUnregisterTetheringEventCallback(mTetheringEventCallback);
         mTetheringEventCallback = null;
 
-        runAsShell(NETWORK_SETTINGS, () -> mTetheredInterfaceRequester.release());
         setIncludeTestInterfaces(false);
     }
 
     @After
     public void tearDown() throws Exception {
+        if (mTetheredInterfaceRequester != null) {
+            mTetheredInterfaceRequester.release();
+        }
         try {
             if (mRunTests) cleanUp();
         } finally {
@@ -263,33 +264,17 @@
         }
     }
 
-    protected static boolean isInterfaceForTetheringAvailable() throws Exception {
-        // Before T, all ethernet interfaces could be used for server mode. Instead of
-        // waiting timeout, just checking whether the system currently has any
-        // ethernet interface is more reliable.
-        if (!SdkLevel.isAtLeastT()) {
-            return runAsShell(CONNECTIVITY_USE_RESTRICTED_NETWORKS, () -> sEm.isAvailable());
-        }
-
+    protected boolean isInterfaceForTetheringAvailable() throws Exception {
         // If previous test case doesn't release tethering interface successfully, the other tests
         // after that test may be skipped as unexcepted.
         // TODO: figure out a better way to check default tethering interface existenion.
-        final TetheredInterfaceRequester requester = new TetheredInterfaceRequester();
-        try {
-            // Use short timeout (200ms) for requesting an existing interface, if any, because
-            // it should reurn faster than requesting a new tethering interface. Using default
-            // timeout (5000ms, TIMEOUT_MS) may make that total testing time is over 1 minute
-            // test module timeout on internal testing.
-            // TODO: if this becomes flaky, consider using default timeout (5000ms) and moving
-            // this check into #setUpOnce.
-            return requester.getInterface(AVAILABLE_TETHER_IFACE_REQUEST_TIMEOUT_MS) != null;
-        } catch (TimeoutException e) {
-            return false;
-        } finally {
-            runAsShell(NETWORK_SETTINGS, () -> {
-                requester.release();
-            });
-        }
+        // Use short timeout (200ms) for requesting an existing interface, if any, because
+        // it should reurn faster than requesting a new tethering interface. Using default
+        // timeout (5000ms, TIMEOUT_MS) may make that total testing time is over 1 minute
+        // test module timeout on internal testing.
+        // TODO: if this becomes flaky, consider using default timeout (5000ms) and moving
+        // this check into #setUpOnce.
+        return mTetheredInterfaceRequester.isPhysicalInterfaceAvailable(SHORT_TIMEOUT_MS);
     }
 
     protected static void setIncludeTestInterfaces(boolean include) {
@@ -304,14 +289,6 @@
         });
     }
 
-    protected String getTetheredInterface() throws Exception {
-        return mTetheredInterfaceRequester.getInterface();
-    }
-
-    protected CompletableFuture<String> requestTetheredInterface() throws Exception {
-        return mTetheredInterfaceRequester.requestInterface();
-    }
-
     protected static void waitForRouterAdvertisement(TapPacketReader reader, String iface,
             long timeoutMs) {
         final long deadline = SystemClock.uptimeMillis() + timeoutMs;
@@ -605,6 +582,11 @@
         private TetheredInterfaceRequest mRequest;
         private final CompletableFuture<String> mFuture = new CompletableFuture<>();
 
+        TetheredInterfaceRequester() {
+            mRequest = runAsShell(NETWORK_SETTINGS, () ->
+                    sEm.requestTetheredInterface(c -> c.run() /* executor */, this));
+        }
+
         @Override
         public void onAvailable(String iface) {
             Log.d(TAG, "Ethernet interface available: " + iface);
@@ -616,28 +598,21 @@
             mFuture.completeExceptionally(new IllegalStateException("onUnavailable received"));
         }
 
-        public CompletableFuture<String> requestInterface() {
-            assertNull("BUG: more than one tethered interface request", mRequest);
-            Log.d(TAG, "Requesting tethered interface");
-            mRequest = runAsShell(NETWORK_SETTINGS, () ->
-                    sEm.requestTetheredInterface(c -> c.run() /* executor */, this));
-            return mFuture;
-        }
-
-        public String getInterface(int timeout) throws Exception {
-            return requestInterface().get(timeout, TimeUnit.MILLISECONDS);
+        public boolean isPhysicalInterfaceAvailable(int timeout) {
+            try {
+                final String iface = mFuture.get(timeout, TimeUnit.MILLISECONDS);
+                return !iface.startsWith("testtap");
+            } catch (Exception e) {
+                return false;
+            }
         }
 
         public String getInterface() throws Exception {
-            return getInterface(TIMEOUT_MS);
+            return mFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
         }
 
         public void release() {
-            if (mRequest != null) {
-                mFuture.obtrudeException(new IllegalStateException("Request already released"));
-                mRequest.release();
-                mRequest = null;
-            }
+            runAsShell(NETWORK_SETTINGS, () -> mRequest.release());
         }
     }
 
@@ -658,7 +633,10 @@
         lp.setLinkAddresses(addresses);
         lp.setDnsServers(dnses);
 
-        return runAsShell(MANAGE_TEST_NETWORKS, () -> initTestNetwork(sContext, lp, TIMEOUT_MS));
+        // TODO: initTestNetwork can take up to 15 seconds on a workstation. Investigate when and
+        // why this is the case. It is unclear whether a 30 second timeout is enough when running
+        // these tests in the much slower test infra.
+        return runAsShell(MANAGE_TEST_NETWORKS, () -> initTestNetwork(sContext, lp, 30_000));
     }
 
     protected void sendDownloadPacketUdp(@NonNull final InetAddress srcIp,
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index c54d1b4..5c258b2 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -59,7 +59,7 @@
 import com.android.testutils.NetworkStackModuleTest;
 import com.android.testutils.TapPacketReader;
 
-import org.junit.BeforeClass;
+import org.junit.After;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -75,8 +75,6 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.Random;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 
 @RunWith(AndroidJUnit4.class)
@@ -151,33 +149,14 @@
             (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04  /* Address: 1.2.3.4 */
     };
 
-    /** Enable/disable tethering once before running the tests. */
-    @BeforeClass
-    public static void setUpOnce() throws Exception {
-        // The first test case may experience tethering restart with IP conflict handling.
-        // Tethering would cache the last upstreams so that the next enabled tethering avoids
-        // picking up the address that is in conflict with the upstreams. To protect subsequent
-        // tests, turn tethering on and off before running them.
-        MyTetheringEventCallback callback = null;
-        TestNetworkInterface testIface = null;
-        assumeTrue(sEm != null);
-        try {
-            // If the physical ethernet interface is available, do nothing.
-            if (isInterfaceForTetheringAvailable()) return;
-
-            testIface = createTestInterface();
-            setIncludeTestInterfaces(true);
-
-            callback = enableEthernetTethering(testIface.getInterfaceName(), null);
-            callback.awaitUpstreamChanged(true /* throwTimeoutException */);
-        } catch (TimeoutException e) {
-            Log.d(TAG, "WARNNING " + e);
-        } finally {
-            maybeCloseTestInterface(testIface);
-            maybeUnregisterTetheringEventCallback(callback);
-
-            setIncludeTestInterfaces(false);
-        }
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+        // TODO: See b/318121782#comment4. Register an ethernet InterfaceStateListener, and wait for
+        // the callback to report client mode. This happens as soon as both
+        // TetheredInterfaceRequester and the tethering code itself have released the interface,
+        // i.e. after stopTethering() has completed.
+        Thread.sleep(3000);
     }
 
     @Test
@@ -201,7 +180,7 @@
             Log.d(TAG, "Including test interfaces");
             setIncludeTestInterfaces(true);
 
-            final String iface = getTetheredInterface();
+            final String iface = mTetheredInterfaceRequester.getInterface();
             assertEquals("TetheredInterfaceCallback for unexpected interface",
                     downstreamIface.getInterfaceName(), iface);
 
@@ -223,8 +202,6 @@
         // This test requires manipulating packets. Skip if there is a physical Ethernet connected.
         assumeFalse(isInterfaceForTetheringAvailable());
 
-        CompletableFuture<String> futureIface = requestTetheredInterface();
-
         setIncludeTestInterfaces(true);
 
         TestNetworkInterface downstreamIface = null;
@@ -234,7 +211,7 @@
         try {
             downstreamIface = createTestInterface();
 
-            final String iface = futureIface.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+            final String iface = mTetheredInterfaceRequester.getInterface();
             assertEquals("TetheredInterfaceCallback for unexpected interface",
                     downstreamIface.getInterfaceName(), iface);
 
@@ -264,7 +241,7 @@
         try {
             downstreamIface = createTestInterface();
 
-            final String iface = getTetheredInterface();
+            final String iface = mTetheredInterfaceRequester.getInterface();
             assertEquals("TetheredInterfaceCallback for unexpected interface",
                     downstreamIface.getInterfaceName(), iface);
 
@@ -338,7 +315,7 @@
         try {
             downstreamIface = createTestInterface();
 
-            final String iface = getTetheredInterface();
+            final String iface = mTetheredInterfaceRequester.getInterface();
             assertEquals("TetheredInterfaceCallback for unexpected interface",
                     downstreamIface.getInterfaceName(), iface);
 
@@ -388,7 +365,7 @@
         MyTetheringEventCallback tetheringEventCallback = null;
         try {
             // Get an interface to use.
-            final String iface = getTetheredInterface();
+            final String iface = mTetheredInterfaceRequester.getInterface();
 
             // Enable Ethernet tethering and check that it starts.
             tetheringEventCallback = enableEthernetTethering(iface, null /* any upstream */);
@@ -509,17 +486,23 @@
         // TODO: test BPF offload maps {rule, stats}.
     }
 
-    // Test network topology:
-    //
-    //         public network (rawip)                 private network
-    //                   |                 UE                |
-    // +------------+    V    +------------+------------+    V    +------------+
-    // |   Sever    +---------+  Upstream  | Downstream +---------+   Client   |
-    // +------------+         +------------+------------+         +------------+
-    // remote ip              public ip                           private ip
-    // 8.8.8.8:443            <Upstream ip>:9876                  <TetheredDevice ip>:9876
-    //
-    private void runUdp4Test() throws Exception {
+
+    /**
+     * Basic IPv4 UDP tethering test. Verify that UDP tethered packets are transferred no matter
+     * using which data path.
+     */
+    @Test
+    public void testTetherUdpV4() throws Exception {
+        // Test network topology:
+        //
+        //         public network (rawip)                 private network
+        //                   |                 UE                |
+        // +------------+    V    +------------+------------+    V    +------------+
+        // |   Sever    +---------+  Upstream  | Downstream +---------+   Client   |
+        // +------------+         +------------+------------+         +------------+
+        // remote ip              public ip                           private ip
+        // 8.8.8.8:443            <Upstream ip>:9876                  <TetheredDevice ip>:9876
+        //
         final TetheringTester tester = initTetheringTester(toList(TEST_IP4_ADDR),
                 toList(TEST_IP4_DNS));
         final TetheredDevice tethered = tester.createTetheredDevice(TEST_MAC, false /* hasIpv6 */);
@@ -541,15 +524,6 @@
         sendDownloadPacketUdp(remoteIp, tetheringUpstreamIp, tester, false /* is6To4 */);
     }
 
-    /**
-     * Basic IPv4 UDP tethering test. Verify that UDP tethered packets are transferred no matter
-     * using which data path.
-     */
-    @Test
-    public void testTetherUdpV4() throws Exception {
-        runUdp4Test();
-    }
-
     // Test network topology:
     //
     //            public network (rawip)                 private network
@@ -599,7 +573,7 @@
         final TetheredDevice tethered = tester.createTetheredDevice(TEST_MAC, false /* hasIpv6 */);
 
         // TODO: remove the connectivity verification for upstream connected notification race.
-        // See the same reason in runUdp4Test().
+        // See the same reason in testTetherUdp4().
         probeV4TetheringConnectivity(tester, tethered, false /* is4To6 */);
 
         final ByteBuffer request = buildIcmpEchoPacketV4(tethered.macAddr /* srcMac */,
@@ -707,7 +681,7 @@
         final TetheredDevice tethered = tester.createTetheredDevice(TEST_MAC, false /* hasIpv6 */);
 
         // TODO: remove the connectivity verification for upstream connected notification race.
-        // See the same reason in runUdp4Test().
+        // See the same reason in testTetherUdp4().
         probeV4TetheringConnectivity(tester, tethered, false /* is4To6 */);
 
         // [1] Send DNS query.
@@ -751,7 +725,7 @@
         final TetheredDevice tethered = tester.createTetheredDevice(TEST_MAC, false /* hasIpv6 */);
 
         // TODO: remove the connectivity verification for upstream connected notification race.
-        // See the same reason in runUdp4Test().
+        // See the same reason in testTetherUdp4().
         probeV4TetheringConnectivity(tester, tethered, false /* is4To6 */);
 
         runTcpTest(tethered.macAddr /* uploadSrcMac */, tethered.routerMacAddr /* uploadDstMac */,
diff --git a/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
index d5d71bc..47aebe8 100644
--- a/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
+++ b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
@@ -22,19 +22,24 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
 
 import android.net.MacAddress;
 import android.os.Build;
+import android.os.SystemClock;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.system.OsConstants;
 import android.util.ArrayMap;
 
 import com.android.net.module.util.BpfMap;
+import com.android.net.module.util.SingleWriterBpfMap;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRunner;
 
@@ -42,10 +47,18 @@
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 
 import java.io.File;
 import java.net.InetAddress;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.NoSuchElementException;
+import java.util.Random;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
 
@@ -56,11 +69,26 @@
     private static final int TEST_MAP_SIZE = 16;
     private static final String TETHER_DOWNSTREAM6_FS_PATH =
             "/sys/fs/bpf/tethering/map_test_tether_downstream6_map";
+    private static final String TETHER2_DOWNSTREAM6_FS_PATH =
+            "/sys/fs/bpf/tethering/map_test_tether2_downstream6_map";
+    private static final String TETHER3_DOWNSTREAM6_FS_PATH =
+            "/sys/fs/bpf/tethering/map_test_tether3_downstream6_map";
 
     private ArrayMap<TetherDownstream6Key, Tether6Value> mTestData;
 
     private BpfMap<TetherDownstream6Key, Tether6Value> mTestMap;
 
+    private final boolean mShouldTestSingleWriterMap;
+
+    @Parameterized.Parameters
+    public static Collection<Boolean> shouldTestSingleWriterMap() {
+        return Arrays.asList(true, false);
+    }
+
+    public BpfMapTest(boolean shouldTestSingleWriterMap) {
+        mShouldTestSingleWriterMap = shouldTestSingleWriterMap;
+    }
+
     @BeforeClass
     public static void setupOnce() {
         System.loadLibrary(getTetheringJniLibraryName());
@@ -82,11 +110,16 @@
         initTestMap();
     }
 
-    private void initTestMap() throws Exception {
-        mTestMap = new BpfMap<>(
-                TETHER_DOWNSTREAM6_FS_PATH,
-                TetherDownstream6Key.class, Tether6Value.class);
+    private BpfMap<TetherDownstream6Key, Tether6Value> openTestMap() throws Exception {
+        return mShouldTestSingleWriterMap
+                ? SingleWriterBpfMap.getSingleton(TETHER2_DOWNSTREAM6_FS_PATH,
+                        TetherDownstream6Key.class, Tether6Value.class)
+                : new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH, TetherDownstream6Key.class,
+                        Tether6Value.class);
+    }
 
+    private void initTestMap() throws Exception {
+        mTestMap = openTestMap();
         mTestMap.forEach((key, value) -> {
             try {
                 assertTrue(mTestMap.deleteEntry(key));
@@ -125,7 +158,7 @@
                 assertEquals(OsConstants.EPERM, expected.errno);
             }
         }
-        try (BpfMap writeOnlyMap = new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH, BpfMap.BPF_F_WRONLY,
+        try (BpfMap writeOnlyMap = new BpfMap<>(TETHER3_DOWNSTREAM6_FS_PATH, BpfMap.BPF_F_WRONLY,
                 TetherDownstream6Key.class, Tether6Value.class)) {
             assertNotNull(writeOnlyMap);
             try {
@@ -357,6 +390,25 @@
     }
 
     @Test
+    public void testMapContentsCorrectOnOpen() throws Exception {
+        final BpfMap<TetherDownstream6Key, Tether6Value> map1, map2;
+
+        map1 = openTestMap();
+        map1.clear();
+        for (int i = 0; i < mTestData.size(); i++) {
+            map1.insertEntry(mTestData.keyAt(i), mTestData.valueAt(i));
+        }
+
+        // We can't close and reopen map1, because close does nothing. Open another map instead.
+        map2 = openTestMap();
+        for (int i = 0; i < mTestData.size(); i++) {
+            assertEquals(mTestData.valueAt(i), map2.getValue(mTestData.keyAt(i)));
+        }
+
+        map1.clear();
+    }
+
+    @Test
     public void testInsertOverflow() throws Exception {
         final ArrayMap<TetherDownstream6Key, Tether6Value> testData =
                 new ArrayMap<>();
@@ -396,8 +448,18 @@
         }
     }
 
-    private static int getNumOpenFds() {
-        return new File("/proc/" + Os.getpid() + "/fd").listFiles().length;
+    private static int getNumOpenBpfMapFds() throws Exception {
+        int numFds = 0;
+        File[] openFiles = new File("/proc/self/fd").listFiles();
+        for (int i = 0; i < openFiles.length; i++) {
+            final Path path = openFiles[i].toPath();
+            if (!Files.isSymbolicLink(path)) continue;
+            if ("anon_inode:bpf-map".equals(Files.readSymbolicLink(path).toString())) {
+                numFds++;
+            }
+        }
+        assertNotEquals("Couldn't find any BPF map fds opened by this process", 0, numFds);
+        return numFds;
     }
 
     @Test
@@ -406,7 +468,7 @@
         // cache, expect that the fd amount is not increased in the iterations.
         // See the comment of BpfMap#close.
         final int iterations = 1000;
-        final int before = getNumOpenFds();
+        final int before = getNumOpenBpfMapFds();
         for (int i = 0; i < iterations; i++) {
             try (BpfMap<TetherDownstream6Key, Tether6Value> map = new BpfMap<>(
                     TETHER_DOWNSTREAM6_FS_PATH,
@@ -414,15 +476,74 @@
                 // do nothing
             }
         }
-        final int after = getNumOpenFds();
+        final int after = getNumOpenBpfMapFds();
 
         // Check that the number of open fds is the same as before.
-        // If this exact match becomes flaky, we probably need to distinguish that fd is belong
-        // to "bpf-map".
-        // ex:
-        // $ adb shell ls -all /proc/16196/fd
-        // [..] network_stack 64 2022-07-26 22:01:02.300002956 +0800 749 -> anon_inode:bpf-map
-        // [..] network_stack 64 2022-07-26 22:01:02.188002956 +0800 75 -> anon_inode:[eventfd]
         assertEquals("Fd leak after " + iterations + " iterations: ", before, after);
     }
+
+    @Test
+    public void testNullKey() {
+        assertThrows(NullPointerException.class, () ->
+                mTestMap.insertOrReplaceEntry(null, mTestData.valueAt(0)));
+    }
+
+    private void runBenchmarkThread(BpfMap<TetherDownstream6Key, Tether6Value> map,
+            CompletableFuture<Integer> future, int runtimeMs) {
+        int numReads = 0;
+        final Random r = new Random();
+        final long start = SystemClock.elapsedRealtime();
+        final long stop = start + runtimeMs;
+        while (SystemClock.elapsedRealtime() < stop) {
+            try {
+                final Tether6Value v = map.getValue(mTestData.keyAt(r.nextInt(mTestData.size())));
+                assertNotNull(v);
+                numReads++;
+            } catch (Exception e) {
+                future.completeExceptionally(e);
+                return;
+            }
+        }
+        future.complete(numReads);
+    }
+
+    @Test
+    public void testSingleWriterCacheEffectiveness() throws Exception {
+        assumeTrue(mShouldTestSingleWriterMap);
+        // Benchmark parameters.
+        final int timeoutMs = 5_000;  // Only hit if threads don't complete.
+        final int benchmarkTimeMs = 2_000;
+        final int minReads = 50;
+        // Local testing on cuttlefish suggests that caching is ~10x faster.
+        // Only require 3x to reduce test flakiness.
+        final int expectedSpeedup = 3;
+
+        final BpfMap cachedMap = SingleWriterBpfMap.getSingleton(TETHER2_DOWNSTREAM6_FS_PATH,
+                TetherDownstream6Key.class, Tether6Value.class);
+        final BpfMap uncachedMap = new BpfMap(TETHER_DOWNSTREAM6_FS_PATH,
+                TetherDownstream6Key.class, Tether6Value.class);
+
+        // Ensure the maps are not empty.
+        for (int i = 0; i < mTestData.size(); i++) {
+            cachedMap.insertEntry(mTestData.keyAt(i), mTestData.valueAt(i));
+            uncachedMap.insertEntry(mTestData.keyAt(i), mTestData.valueAt(i));
+        }
+
+        final CompletableFuture<Integer> cachedResult = new CompletableFuture<>();
+        final CompletableFuture<Integer> uncachedResult = new CompletableFuture<>();
+
+        new Thread(() -> runBenchmarkThread(uncachedMap, uncachedResult, benchmarkTimeMs)).start();
+        new Thread(() -> runBenchmarkThread(cachedMap, cachedResult, benchmarkTimeMs)).start();
+
+        final int cached = cachedResult.get(timeoutMs, TimeUnit.MILLISECONDS);
+        final int uncached = uncachedResult.get(timeoutMs, TimeUnit.MILLISECONDS);
+
+        // Uncomment to see benchmark results.
+        // fail("Cached " + cached + ", uncached " + uncached + ": " + cached / uncached  +"x");
+
+        assertTrue("Less than " + minReads + "cached reads observed", cached > minReads);
+        assertTrue("Less than " + minReads + "uncached reads observed", uncached > minReads);
+        assertTrue("Cached map not at least " + expectedSpeedup + "x faster",
+                cached > expectedSpeedup * uncached);
+    }
 }
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
index 1958aa8..9e67415 100644
--- a/bpf_progs/Android.bp
+++ b/bpf_progs/Android.bp
@@ -85,6 +85,16 @@
 }
 
 bpf {
+    name: "gentle.o",
+    srcs: ["gentle.c"],
+    cflags: [
+        "-Wall",
+        "-Werror",
+    ],
+    sub_dir: "net_shared",
+}
+
+bpf {
     name: "offload.o",
     srcs: ["offload.c"],
     cflags: [
diff --git a/bpf_progs/block.c b/bpf_progs/block.c
index 152dda6..353525e 100644
--- a/bpf_progs/block.c
+++ b/bpf_progs/block.c
@@ -20,7 +20,7 @@
 #include <stdint.h>
 
 // The resulting .o needs to load on Android T+
-#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
+#define BPFLOADER_MIN_VER BPFLOADER_T_VERSION
 
 #include "bpf_helpers.h"
 
@@ -76,3 +76,4 @@
 LICENSE("Apache 2.0");
 CRITICAL("ConnectivityNative");
 DISABLE_BTF_ON_USER_BUILDS();
+DISABLE_ON_MAINLINE_BEFORE_U_QPR3();
diff --git a/bpf_progs/bpf_net_helpers.h b/bpf_progs/bpf_net_helpers.h
index f3c7de5..1511ee5 100644
--- a/bpf_progs/bpf_net_helpers.h
+++ b/bpf_progs/bpf_net_helpers.h
@@ -35,6 +35,7 @@
 
 // this returns 0 iff skb->sk is NULL
 static uint64_t (*bpf_get_socket_cookie)(struct __sk_buff* skb) = (void*)BPF_FUNC_get_socket_cookie;
+static uint64_t (*bpf_get_sk_cookie)(struct bpf_sock* sk) = (void*)BPF_FUNC_get_socket_cookie;
 
 static uint32_t (*bpf_get_socket_uid)(struct __sk_buff* skb) = (void*)BPF_FUNC_get_socket_uid;
 
diff --git a/bpf_progs/clatd.c b/bpf_progs/clatd.c
index f83e5ae..95e9a4c 100644
--- a/bpf_progs/clatd.c
+++ b/bpf_progs/clatd.c
@@ -31,7 +31,7 @@
 #include <linux/udp.h>
 
 // The resulting .o needs to load on Android T+
-#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
+#define BPFLOADER_MIN_VER BPFLOADER_T_VERSION
 
 #include "bpf_helpers.h"
 #include "bpf_net_helpers.h"
@@ -431,3 +431,4 @@
 LICENSE("Apache 2.0");
 CRITICAL("Connectivity");
 DISABLE_BTF_ON_USER_BUILDS();
+DISABLE_ON_MAINLINE_BEFORE_U_QPR3();
diff --git a/bpf_progs/dscpPolicy.c b/bpf_progs/dscpPolicy.c
index ed114e4..8bb8ad5 100644
--- a/bpf_progs/dscpPolicy.c
+++ b/bpf_progs/dscpPolicy.c
@@ -28,7 +28,7 @@
 #include <string.h>
 
 // The resulting .o needs to load on Android T+
-#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
+#define BPFLOADER_MIN_VER BPFLOADER_T_VERSION
 
 #include "bpf_helpers.h"
 #include "dscpPolicy.h"
@@ -239,3 +239,4 @@
 LICENSE("Apache 2.0");
 CRITICAL("Connectivity");
 DISABLE_BTF_ON_USER_BUILDS();
+DISABLE_ON_MAINLINE_BEFORE_U_QPR3();
diff --git a/bpf_progs/gentle.c b/bpf_progs/gentle.c
new file mode 100644
index 0000000..ab9620e
--- /dev/null
+++ b/bpf_progs/gentle.c
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 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 BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
+
+#include "bpf_helpers.h"
+#include "bpf_net_helpers.h"
+
+DEFINE_BPF_MAP_GRW(test, ARRAY, int, uint64_t, 1, AID_SYSTEM)
+
+DEFINE_BPF_PROG("skfilter/accept", AID_ROOT, AID_SYSTEM, accept)
+(struct __sk_buff *skb) {
+    return 1;
+}
+
+LICENSE("Apache 2.0");
+DISABLE_BTF_ON_USER_BUILDS();
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index c3acaad..5ce2227 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -15,7 +15,7 @@
  */
 
 // The resulting .o needs to load on Android T+
-#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
+#define BPFLOADER_MIN_VER BPFLOADER_T_VERSION
 
 #include <bpf_helpers.h>
 #include <linux/bpf.h>
@@ -97,19 +97,22 @@
 DEFINE_BPF_MAP_NO_NETD(ingress_discard_map, HASH, IngressDiscardKey, IngressDiscardValue,
                        INGRESS_DISCARD_MAP_SIZE)
 
+DEFINE_BPF_MAP_RW_NETD(lock_array_test_map, ARRAY, uint32_t, bool, 1)
+DEFINE_BPF_MAP_RW_NETD(lock_hash_test_map, HASH, uint32_t, bool, 1)
+
 /* never actually used from ebpf */
 DEFINE_BPF_MAP_NO_NETD(iface_index_name_map, HASH, uint32_t, IfaceValue, IFACE_INDEX_NAME_MAP_SIZE)
 
 // 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", "", PRIVATE,
-                   BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG,
+                   BPFLOADER_U_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG,
                    LOAD_ON_USER, LOAD_ON_USERDEBUG)
 
 // 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", "", PRIVATE,
-                       BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG,
+                       BPFLOADER_U_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG,
                        LOAD_ON_USER, LOAD_ON_USERDEBUG);
 
 DEFINE_BPF_MAP_RO_NETD(data_saver_enabled_map, ARRAY, uint32_t, bool,
@@ -139,6 +142,11 @@
 #define DEFINE_NETD_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
     DEFINE_NETD_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, KVER_NONE)
 
+#define DEFINE_NETD_V_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, minKV)            \
+    DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, minKV,                        \
+                        KVER_INF, BPFLOADER_MAINLINE_V_VERSION, BPFLOADER_MAX_VER, MANDATORY,     \
+                        "fs_bpf_netd_readonly", "", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+
 // 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,  \
@@ -519,7 +527,7 @@
 // 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_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, OPTIONAL,
+                    BPFLOADER_U_VERSION, BPFLOADER_MAX_VER, OPTIONAL,
                     "fs_bpf_netd_readonly", "",
                     IGNORE_ON_ENG, LOAD_ON_USER, IGNORE_ON_USERDEBUG)
 (struct __sk_buff* skb) {
@@ -529,7 +537,7 @@
 // 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_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, MANDATORY,
+                    BPFLOADER_U_VERSION, BPFLOADER_MAX_VER, MANDATORY,
                     "fs_bpf_netd_readonly", "",
                     LOAD_ON_ENG, IGNORE_ON_USER, LOAD_ON_USERDEBUG)
 (struct __sk_buff* skb) {
@@ -551,7 +559,7 @@
 // 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_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, OPTIONAL,
+                    BPFLOADER_U_VERSION, BPFLOADER_MAX_VER, OPTIONAL,
                     "fs_bpf_netd_readonly", "",
                     IGNORE_ON_ENG, LOAD_ON_USER, IGNORE_ON_USERDEBUG)
 (struct __sk_buff* skb) {
@@ -561,7 +569,7 @@
 // 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_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, MANDATORY,
+                    BPFLOADER_U_VERSION, BPFLOADER_MAX_VER, MANDATORY,
                     "fs_bpf_netd_readonly", "",
                     LOAD_ON_ENG, IGNORE_ON_USER, LOAD_ON_USERDEBUG)
 (struct __sk_buff* skb) {
@@ -666,13 +674,87 @@
     return permissions ? *permissions : BPF_PERMISSION_INTERNET;
 }
 
-DEFINE_NETD_BPF_PROG_KVER("cgroupsock/inet/create", AID_ROOT, AID_ROOT, inet_socket_create,
+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 (get_app_permissions() & BPF_PERMISSION_INTERNET) ? 1 : 0;
 }
 
+DEFINE_NETD_V_BPF_PROG_KVER("cgroupsockrelease/inet_release", AID_ROOT, AID_ROOT,
+                            inet_socket_release, KVER_5_10)
+(struct bpf_sock* sk) {
+    uint64_t cookie = bpf_get_sk_cookie(sk);
+    if (cookie) bpf_cookie_tag_map_delete_elem(&cookie);
+
+    return 1;
+}
+
+static __always_inline inline int check_localhost(struct bpf_sock_addr *ctx) {
+    // See include/uapi/linux/bpf.h:
+    //
+    // struct bpf_sock_addr {
+    //   __u32 user_family;	//     R: 4 byte
+    //   __u32 user_ip4;	// BE, R: 1,2,4-byte,   W: 4-byte
+    //   __u32 user_ip6[4];	// BE, R: 1,2,4,8-byte, W: 4,8-byte
+    //   __u32 user_port;	// BE, R: 1,2,4-byte,   W: 4-byte
+    //   __u32 family;		//     R: 4 byte
+    //   __u32 type;		//     R: 4 byte
+    //   __u32 protocol;	//     R: 4 byte
+    //   __u32 msg_src_ip4;	// BE, R: 1,2,4-byte,   W: 4-byte
+    //   __u32 msg_src_ip6[4];	// BE, R: 1,2,4,8-byte, W: 4,8-byte
+    //   __bpf_md_ptr(struct bpf_sock *, sk);
+    // };
+    return 1;
+}
+
+DEFINE_NETD_V_BPF_PROG_KVER("connect4/inet4_connect", AID_ROOT, AID_ROOT, inet4_connect, KVER_4_14)
+(struct bpf_sock_addr *ctx) {
+    return check_localhost(ctx);
+}
+
+DEFINE_NETD_V_BPF_PROG_KVER("connect6/inet6_connect", AID_ROOT, AID_ROOT, inet6_connect, KVER_4_14)
+(struct bpf_sock_addr *ctx) {
+    return check_localhost(ctx);
+}
+
+DEFINE_NETD_V_BPF_PROG_KVER("recvmsg4/udp4_recvmsg", AID_ROOT, AID_ROOT, udp4_recvmsg, KVER_4_14)
+(struct bpf_sock_addr *ctx) {
+    return check_localhost(ctx);
+}
+
+DEFINE_NETD_V_BPF_PROG_KVER("recvmsg6/udp6_recvmsg", AID_ROOT, AID_ROOT, udp6_recvmsg, KVER_4_14)
+(struct bpf_sock_addr *ctx) {
+    return check_localhost(ctx);
+}
+
+DEFINE_NETD_V_BPF_PROG_KVER("sendmsg4/udp4_sendmsg", AID_ROOT, AID_ROOT, udp4_sendmsg, KVER_4_14)
+(struct bpf_sock_addr *ctx) {
+    return check_localhost(ctx);
+}
+
+DEFINE_NETD_V_BPF_PROG_KVER("sendmsg6/udp6_sendmsg", AID_ROOT, AID_ROOT, udp6_sendmsg, KVER_4_14)
+(struct bpf_sock_addr *ctx) {
+    return check_localhost(ctx);
+}
+
+DEFINE_NETD_V_BPF_PROG_KVER("getsockopt/prog", AID_ROOT, AID_ROOT, getsockopt_prog, KVER_5_4)
+(struct bpf_sockopt *ctx) {
+    // Tell kernel to return 'original' kernel reply (instead of the bpf modified buffer)
+    // This is important if the answer is larger than PAGE_SIZE (max size this bpf hook can provide)
+    ctx->optlen = 0;
+    return 1; // ALLOW
+}
+
+DEFINE_NETD_V_BPF_PROG_KVER("setsockopt/prog", AID_ROOT, AID_ROOT, setsockopt_prog, KVER_5_4)
+(struct bpf_sockopt *ctx) {
+    // Tell kernel to use/process original buffer provided by userspace.
+    // This is important if it is larger than PAGE_SIZE (max size this bpf hook can handle).
+    ctx->optlen = 0;
+    return 1; // ALLOW
+}
+
 LICENSE("Apache 2.0");
 CRITICAL("Connectivity and netd");
 DISABLE_BTF_ON_USER_BUILDS();
+DISABLE_ON_MAINLINE_BEFORE_U_QPR3();
diff --git a/bpf_progs/netd.h b/bpf_progs/netd.h
index 8a56b4a..332979b 100644
--- a/bpf_progs/netd.h
+++ b/bpf_progs/netd.h
@@ -155,7 +155,16 @@
 ASSERT_STRING_EQUAL(XT_BPF_ALLOWLIST_PROG_PATH, BPF_NETD_PATH "prog_netd_skfilter_allowlist_xtbpf");
 ASSERT_STRING_EQUAL(XT_BPF_DENYLIST_PROG_PATH,  BPF_NETD_PATH "prog_netd_skfilter_denylist_xtbpf");
 
-#define CGROUP_SOCKET_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupsock_inet_create"
+#define CGROUP_INET_CREATE_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupsock_inet_create"
+#define CGROUP_INET_RELEASE_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupsockrelease_inet_release"
+#define CGROUP_CONNECT4_PROG_PATH BPF_NETD_PATH "prog_netd_connect4_inet4_connect"
+#define CGROUP_CONNECT6_PROG_PATH BPF_NETD_PATH "prog_netd_connect6_inet6_connect"
+#define CGROUP_UDP4_RECVMSG_PROG_PATH BPF_NETD_PATH "prog_netd_recvmsg4_udp4_recvmsg"
+#define CGROUP_UDP6_RECVMSG_PROG_PATH BPF_NETD_PATH "prog_netd_recvmsg6_udp6_recvmsg"
+#define CGROUP_UDP4_SENDMSG_PROG_PATH BPF_NETD_PATH "prog_netd_sendmsg4_udp4_sendmsg"
+#define CGROUP_UDP6_SENDMSG_PROG_PATH BPF_NETD_PATH "prog_netd_sendmsg6_udp6_sendmsg"
+#define CGROUP_GETSOCKOPT_PROG_PATH BPF_NETD_PATH "prog_netd_getsockopt_prog"
+#define CGROUP_SETSOCKOPT_PROG_PATH BPF_NETD_PATH "prog_netd_setsockopt_prog"
 
 #define TC_BPF_INGRESS_ACCOUNT_PROG_NAME "prog_netd_schedact_ingress_account"
 #define TC_BPF_INGRESS_ACCOUNT_PROG_PATH BPF_NETD_PATH TC_BPF_INGRESS_ACCOUNT_PROG_NAME
diff --git a/bpf_progs/offload.c b/bpf_progs/offload.c
index 4f152bf..c7ac059 100644
--- a/bpf_progs/offload.c
+++ b/bpf_progs/offload.c
@@ -28,11 +28,11 @@
 // BTF is incompatible with bpfloaders < v0.10, hence for S (v0.2) we must
 // ship a different file than for later versions, but we need bpfloader v0.25+
 // for obj@ver.o support
-#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
+#define BPFLOADER_MIN_VER BPFLOADER_OBJ_AT_VER_VERSION
 #else /* MAINLINE */
-// The resulting .o needs to load on the Android S bpfloader
+// The resulting .o needs to load on the Android S & T bpfloaders
 #define BPFLOADER_MIN_VER BPFLOADER_S_VERSION
-#define BPFLOADER_MAX_VER BPFLOADER_T_VERSION
+#define BPFLOADER_MAX_VER BPFLOADER_OBJ_AT_VER_VERSION
 #endif /* MAINLINE */
 
 // Warning: values other than AID_ROOT don't work for map uid on BpfLoader < v0.21
@@ -878,3 +878,4 @@
 LICENSE("Apache 2.0");
 CRITICAL("Connectivity (Tethering)");
 DISABLE_BTF_ON_USER_BUILDS();
+DISABLE_ON_MAINLINE_BEFORE_U_QPR3();
diff --git a/bpf_progs/test.c b/bpf_progs/test.c
index fff3512..6a4471c 100644
--- a/bpf_progs/test.c
+++ b/bpf_progs/test.c
@@ -45,6 +45,10 @@
 // Used only by TetheringPrivilegedTests, not by production code.
 DEFINE_BPF_MAP_GRW(tether_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 16,
                    TETHERING_GID)
+DEFINE_BPF_MAP_GRW(tether2_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 16,
+                   TETHERING_GID)
+DEFINE_BPF_MAP_GRW(tether3_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 16,
+                   TETHERING_GID)
 // Used only by BpfBitmapTest, not by production code.
 DEFINE_BPF_MAP_GRW(bitmap, ARRAY, int, uint64_t, 2, TETHERING_GID)
 
diff --git a/common/flags.aconfig b/common/flags.aconfig
index 6c3e89d..b320b61 100644
--- a/common/flags.aconfig
+++ b/common/flags.aconfig
@@ -107,3 +107,19 @@
   description: "Flag for BLOCKED_REASON_NETWORK_RESTRICTED API"
   bug: "339559837"
 }
+
+flag {
+  name: "net_capability_not_bandwidth_constrained"
+  is_exported: true
+  namespace: "android_core_networking"
+  description: "Flag for NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED API"
+  bug: "343823469"
+}
+
+flag {
+  name: "tethering_request_virtual"
+  is_exported: true
+  namespace: "android_core_networking"
+  description: "Flag for introducing TETHERING_VIRTUAL type"
+  bug: "340376953"
+}
diff --git a/framework/api/current.txt b/framework/api/current.txt
index ef8415c..7bc0cf3 100644
--- a/framework/api/current.txt
+++ b/framework/api/current.txt
@@ -337,6 +337,7 @@
     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 @FlaggedApi("com.android.net.flags.net_capability_not_bandwidth_constrained") public static final int NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED = 37; // 0x25
     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
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 0b37fa5..4eaf973 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -28,6 +28,7 @@
 import android.annotation.CallbackExecutor;
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
+import android.annotation.LongDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresApi;
@@ -1207,6 +1208,16 @@
     })
     public @interface FirewallRule {}
 
+    /** @hide */
+    public static final long FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS = 1L;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @LongDef(flag = true, prefix = "FEATURE_", value = {
+            FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS
+    })
+    public @interface ConnectivityManagerFeature {}
+
     /**
      * A kludge to facilitate static access where a Context pointer isn't available, like in the
      * case of the static set/getProcessDefaultNetwork methods and from the Network class.
@@ -1220,6 +1231,14 @@
     @GuardedBy("mTetheringEventCallbacks")
     private TetheringManager mTetheringManager;
 
+    private final Object mEnabledConnectivityManagerFeaturesLock = new Object();
+    // mEnabledConnectivityManagerFeatures is lazy-loaded in this ConnectivityManager instance, but
+    // fetched from ConnectivityService, where it is loaded in ConnectivityService startup, so it
+    // should have consistent values.
+    @GuardedBy("sEnabledConnectivityManagerFeaturesLock")
+    @ConnectivityManagerFeature
+    private Long mEnabledConnectivityManagerFeatures = null;
+
     private TetheringManager getTetheringManager() {
         synchronized (mTetheringEventCallbacks) {
             if (mTetheringManager == null) {
@@ -4529,6 +4548,20 @@
         return request;
     }
 
+    private boolean isFeatureEnabled(@ConnectivityManagerFeature long connectivityManagerFeature) {
+        synchronized (mEnabledConnectivityManagerFeaturesLock) {
+            if (mEnabledConnectivityManagerFeatures == null) {
+                try {
+                    mEnabledConnectivityManagerFeatures =
+                            mService.getEnabledConnectivityManagerFeatures();
+                } catch (RemoteException e) {
+                    e.rethrowFromSystemServer();
+                }
+            }
+            return (mEnabledConnectivityManagerFeatures & connectivityManagerFeature) != 0;
+        }
+    }
+
     private NetworkRequest sendRequestForNetwork(NetworkCapabilities need, NetworkCallback callback,
             int timeoutMs, NetworkRequest.Type reqType, int legacyType, CallbackHandler handler) {
         return sendRequestForNetwork(Process.INVALID_UID, need, callback, timeoutMs, reqType,
diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl
index 55c7085..f9de8ed 100644
--- a/framework/src/android/net/IConnectivityManager.aidl
+++ b/framework/src/android/net/IConnectivityManager.aidl
@@ -259,4 +259,6 @@
     void setTestLowTcpPollingTimerForKeepalive(long timeMs);
 
     IBinder getRoutingCoordinatorService();
+
+    long getEnabledConnectivityManagerFeatures();
 }
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index 45efbfe..6a14bde 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -36,9 +36,11 @@
 import android.os.Process;
 import android.text.TextUtils;
 import android.util.ArraySet;
+import android.util.Log;
 import android.util.Range;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.BitUtils;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.NetworkCapabilitiesUtils;
@@ -134,6 +136,8 @@
                 "com.android.net.flags.request_restricted_wifi";
         static final String SUPPORT_TRANSPORT_SATELLITE =
                 "com.android.net.flags.support_transport_satellite";
+        static final String NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED =
+                "com.android.net.flags.net_capability_not_bandwidth_constrained";
     }
 
     /**
@@ -458,6 +462,7 @@
             NET_CAPABILITY_PRIORITIZE_LATENCY,
             NET_CAPABILITY_PRIORITIZE_BANDWIDTH,
             NET_CAPABILITY_LOCAL_NETWORK,
+            NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED,
     })
     public @interface NetCapability { }
 
@@ -740,7 +745,26 @@
     @FlaggedApi(Flags.FLAG_NET_CAPABILITY_LOCAL_NETWORK)
     public static final int NET_CAPABILITY_LOCAL_NETWORK = 36;
 
-    private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_LOCAL_NETWORK;
+    /**
+     * Indicates that this is not a bandwidth-constrained network.
+     *
+     * Starting from {@link Build.VERSION_CODES.VANILLA_ICE_CREAM}, this capability is by default
+     * set in {@link NetworkRequest}s and true for most networks.
+     *
+     * If a network lacks this capability, it is bandwidth-constrained. Bandwidth constrained
+     * networks cannot support high-bandwidth data transfers and applications that request and use
+     * them must ensure that they limit bandwidth usage to below the values returned by
+     * {@link #getLinkDownstreamBandwidthKbps()} and {@link #getLinkUpstreamBandwidthKbps()} and
+     * limit the frequency of their network usage. If applications perform high-bandwidth data
+     * transfers on constrained networks or perform network access too frequently, the system may
+     * block the app's access to the network. The system may take other measures to reduce network
+     * usage on constrained networks, such as disabling network access to apps that are not in the
+     * foreground.
+     */
+    @FlaggedApi(Flags.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
+    public static final int NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED = 37;
+
+    private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED;
 
     // Set all bits up to the MAX_NET_CAPABILITY-th bit
     private static final long ALL_VALID_CAPABILITIES = (2L << MAX_NET_CAPABILITY) - 1;
@@ -784,10 +808,17 @@
     /**
      * Capabilities that are set by default when the object is constructed.
      */
-    private static final long DEFAULT_CAPABILITIES =
-            (1L << NET_CAPABILITY_NOT_RESTRICTED) |
-            (1L << NET_CAPABILITY_TRUSTED) |
-            (1L << NET_CAPABILITY_NOT_VPN);
+    private static final long DEFAULT_CAPABILITIES;
+    static {
+        long defaultCapabilities =
+                (1L << NET_CAPABILITY_NOT_RESTRICTED)
+                | (1L << NET_CAPABILITY_TRUSTED)
+                | (1L << NET_CAPABILITY_NOT_VPN);
+        if (SdkLevel.isAtLeastV()) {
+            defaultCapabilities |= (1L << NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED);
+        }
+        DEFAULT_CAPABILITIES = defaultCapabilities;
+    }
 
     /**
      * Capabilities that are managed by ConnectivityService.
@@ -814,7 +845,9 @@
             (1L << NET_CAPABILITY_NOT_ROAMING) |
             (1L << NET_CAPABILITY_NOT_CONGESTED) |
             (1L << NET_CAPABILITY_NOT_SUSPENDED) |
-            (1L << NET_CAPABILITY_NOT_VCN_MANAGED);
+            (1L << NET_CAPABILITY_NOT_VCN_MANAGED) |
+            (1L << NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED);
+
 
     /**
      * Extra allowed capabilities for test networks that do not have TRANSPORT_CELLULAR. Test
@@ -843,7 +876,10 @@
         // 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: Add forbidden capabilities to the public API
-        checkValidCapability(capability);
+        if (!isValidCapability(capability)) {
+            Log.e(TAG, "addCapability is called with invalid capability: " + capability);
+            return this;
+        }
         mNetworkCapabilities |= 1L << capability;
         // remove from forbidden capability list
         mForbiddenNetworkCapabilities &= ~(1L << capability);
@@ -864,7 +900,10 @@
      * @hide
      */
     public void addForbiddenCapability(@NetCapability int capability) {
-        checkValidCapability(capability);
+        if (!isValidCapability(capability)) {
+            Log.e(TAG, "addForbiddenCapability is called with invalid capability: " + capability);
+            return;
+        }
         mForbiddenNetworkCapabilities |= 1L << capability;
         mNetworkCapabilities &= ~(1L << capability);  // remove from requested capabilities
     }
@@ -878,7 +917,10 @@
      * @hide
      */
     public @NonNull NetworkCapabilities removeCapability(@NetCapability int capability) {
-        checkValidCapability(capability);
+        if (!isValidCapability(capability)) {
+            Log.e(TAG, "removeCapability is called with invalid capability: " + capability);
+            return this;
+        }
         final long mask = ~(1L << capability);
         mNetworkCapabilities &= mask;
         return this;
@@ -893,7 +935,11 @@
      * @hide
      */
     public @NonNull NetworkCapabilities removeForbiddenCapability(@NetCapability int capability) {
-        checkValidCapability(capability);
+        if (!isValidCapability(capability)) {
+            Log.e(TAG,
+                    "removeForbiddenCapability is called with invalid capability: " + capability);
+            return this;
+        }
         mForbiddenNetworkCapabilities &= ~(1L << capability);
         return this;
     }
@@ -2589,6 +2635,7 @@
             case NET_CAPABILITY_PRIORITIZE_LATENCY:          return "PRIORITIZE_LATENCY";
             case NET_CAPABILITY_PRIORITIZE_BANDWIDTH:        return "PRIORITIZE_BANDWIDTH";
             case NET_CAPABILITY_LOCAL_NETWORK:        return "LOCAL_NETWORK";
+            case NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED:    return "NOT_BANDWIDTH_CONSTRAINED";
             default:                                  return Integer.toString(capability);
         }
     }
@@ -2632,12 +2679,6 @@
         return capability >= 0 && capability <= MAX_NET_CAPABILITY;
     }
 
-    private static void checkValidCapability(@NetworkCapabilities.NetCapability int capability) {
-        if (!isValidCapability(capability)) {
-            throw new IllegalArgumentException("NetworkCapability " + capability + " out of range");
-        }
-    }
-
     private static boolean isValidEnterpriseId(
             @NetworkCapabilities.EnterpriseId int enterpriseId) {
         return enterpriseId >= NET_ENTERPRISE_ID_1
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index f7600b2..502ac6f 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -20,6 +20,7 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
@@ -286,7 +287,8 @@
                 NET_CAPABILITY_PARTIAL_CONNECTIVITY,
                 NET_CAPABILITY_TEMPORARILY_NOT_METERED,
                 NET_CAPABILITY_TRUSTED,
-                NET_CAPABILITY_VALIDATED);
+                NET_CAPABILITY_VALIDATED,
+                NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED);
 
         private final NetworkCapabilities mNetworkCapabilities;
 
diff --git a/framework/src/android/net/apf/ApfCapabilities.java b/framework/src/android/net/apf/ApfCapabilities.java
index 6b18629..f92cdbb 100644
--- a/framework/src/android/net/apf/ApfCapabilities.java
+++ b/framework/src/android/net/apf/ApfCapabilities.java
@@ -106,6 +106,8 @@
 
     @Override
     public int hashCode() {
+        // hashCode it is not implemented in R. Therefore it would be dangerous for
+        // NetworkStack to depend on it.
         return Objects.hash(apfVersionSupported, maximumApfProgramSize, apfPacketFormat);
     }
 
diff --git a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
index c726dab..51df8ab 100644
--- a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
+++ b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
@@ -121,6 +121,20 @@
     @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
     public static final long NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION = 333340911L;
 
+    /**
+     * Enable caching for TrafficStats#get* APIs.
+     *
+     * Apps targeting Android V or later or running on Android V or later may take up to several
+     * seconds to see the updated results.
+     * Apps targeting lower android SDKs do not see cached result for backward compatibility,
+     * results of TrafficStats#get* APIs are reflecting network statistics immediately.
+     *
+     * @hide
+     */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public static final long ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE = 74210811L;
+
     private ConnectivityCompatChanges() {
     }
 }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
index 576e806..1e36676 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
@@ -76,7 +76,7 @@
 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class NearbyManagerTest {
 
-    @ClassRule static final EnableBluetoothRule sEnableBluetooth = new EnableBluetoothRule();
+    @ClassRule public static final EnableBluetoothRule sEnableBluetooth = new EnableBluetoothRule();
 
     private static final byte[] SALT = new byte[]{1, 2};
     private static final byte[] SECRET_ID = new byte[]{1, 2, 3, 4};
diff --git a/netbpfload/Android.bp b/netbpfload/Android.bp
index f278695..908bb13 100644
--- a/netbpfload/Android.bp
+++ b/netbpfload/Android.bp
@@ -70,8 +70,8 @@
 // For details of versioned rc files see:
 // https://android.googlesource.com/platform/system/core/+/HEAD/init/README.md#versioned-rc-files-within-apexs
 prebuilt_etc {
-    name: "netbpfload.mainline.rc",
-    src: "netbpfload.mainline.rc",
+    name: "netbpfload.33rc",
+    src: "netbpfload.33rc",
     filename: "netbpfload.33rc",
     installable: false,
 }
diff --git a/netbpfload/NetBpfLoad.cpp b/netbpfload/NetBpfLoad.cpp
index f42e8a5..e9c6d8a 100644
--- a/netbpfload/NetBpfLoad.cpp
+++ b/netbpfload/NetBpfLoad.cpp
@@ -224,11 +224,6 @@
     return 0;
 }
 
-static bool isGSI() {
-    // From //system/gsid/libgsi.cpp IsGsiRunning()
-    return !access("/metadata/gsi/dsu/booted", F_OK);
-}
-
 static bool hasGSM() {
     static string ph = base::GetProperty("gsm.current.phone-type", "");
     static bool gsm = (ph != "");
@@ -254,19 +249,55 @@
 }
 
 static int doLoad(char** argv, char * const envp[]) {
-    const int device_api_level = android_get_device_api_level();
-    const bool isAtLeastT = (device_api_level >= __ANDROID_API_T__);
-    const bool isAtLeastU = (device_api_level >= __ANDROID_API_U__);
-    const bool isAtLeastV = (device_api_level >= __ANDROID_API_V__);
+    const bool runningAsRoot = !getuid();  // true iff U QPR3 or V+
+
+    // Any released device will have codename REL instead of a 'real' codename.
+    // For safety: default to 'REL' so we default to unreleased=false on failure.
+    const bool unreleased = (base::GetProperty("ro.build.version.codename", "REL") != "REL");
+
+    // goog/main device_api_level is bumped *way* before aosp/main api level
+    // (the latter only gets bumped during the push of goog/main to aosp/main)
+    //
+    // Since we develop in AOSP, we want it to behave as if it was bumped too.
+    //
+    // Note that AOSP doesn't really have a good api level (for example during
+    // early V dev cycle, it would have *all* of T, some but not all of U, and some V).
+    // One could argue that for our purposes AOSP api level should be infinite or 10000.
+    //
+    // This could also cause api to be increased in goog/main or other branches,
+    // but I can't imagine a case where this would be a problem: the problem
+    // is rather a too low api level, rather than some ill defined high value.
+    // For example as I write this aosp is 34/U, and goog is 35/V,
+    // we want to treat both goog & aosp as 35/V, but it's harmless if we
+    // treat goog as 36 because that value isn't yet defined to mean anything,
+    // and we thus never compare against it.
+    //
+    // Also note that 'android_get_device_api_level()' is what the
+    //   //system/core/init/apex_init_util.cpp
+    // apex init .XXrc parsing code uses for XX filtering.
+    //
+    // That code has a hack to bump <35 to 35 (to force aosp/main to parse .35rc),
+    // but could (should?) perhaps be adjusted to match this.
+    const int effective_api_level = android_get_device_api_level() + (int)unreleased;
+    const bool isAtLeastT = (effective_api_level >= __ANDROID_API_T__);
+    const bool isAtLeastU = (effective_api_level >= __ANDROID_API_U__);
+    const bool isAtLeastV = (effective_api_level >= __ANDROID_API_V__);
 
     // last in U QPR2 beta1
     const bool has_platform_bpfloader_rc = exists("/system/etc/init/bpfloader.rc");
     // first in U QPR2 beta~2
     const bool has_platform_netbpfload_rc = exists("/system/etc/init/netbpfload.rc");
 
-    ALOGI("NetBpfLoad (%s) api:%d/%d kver:%07x (%s) rc:%d%d",
-          argv[0], android_get_application_target_sdk_version(), device_api_level,
-          kernelVersion(), describeArch(),
+    // Version of Network BpfLoader depends on the Android OS version
+    unsigned int bpfloader_ver = 42u;    // [42] BPFLOADER_MAINLINE_VERSION
+    if (isAtLeastT) ++bpfloader_ver;     // [43] BPFLOADER_MAINLINE_T_VERSION
+    if (isAtLeastU) ++bpfloader_ver;     // [44] BPFLOADER_MAINLINE_U_VERSION
+    if (runningAsRoot) ++bpfloader_ver;  // [45] BPFLOADER_MAINLINE_U_QPR3_VERSION
+    if (isAtLeastV) ++bpfloader_ver;     // [46] BPFLOADER_MAINLINE_V_VERSION
+
+    ALOGI("NetBpfLoad v0.%u (%s) api:%d/%d kver:%07x (%s) uid:%d rc:%d%d",
+          bpfloader_ver, argv[0], android_get_device_api_level(), effective_api_level,
+          kernelVersion(), describeArch(), getuid(),
           has_platform_bpfloader_rc, has_platform_netbpfload_rc);
 
     if (!has_platform_bpfloader_rc && !has_platform_netbpfload_rc) {
@@ -286,22 +317,34 @@
         return 1;
     }
 
+    // both S and T require kernel 4.9 (and eBpf support)
     if (isAtLeastT && !isAtLeastKernelVersion(4, 9, 0)) {
         ALOGE("Android T requires kernel 4.9.");
         return 1;
     }
 
+    // U bumps the kernel requirement up to 4.14
     if (isAtLeastU && !isAtLeastKernelVersion(4, 14, 0)) {
         ALOGE("Android U requires kernel 4.14.");
         return 1;
     }
 
+    // V bumps the kernel requirement up to 4.19
+    // see also: //system/netd/tests/kernel_test.cpp TestKernel419
     if (isAtLeastV && !isAtLeastKernelVersion(4, 19, 0)) {
         ALOGE("Android V requires kernel 4.19.");
         return 1;
     }
 
-    if (isAtLeastV && isX86() && !isKernel64Bit()) {
+    // Technically already required by U, but only enforce on V+
+    // see also: //system/netd/tests/kernel_test.cpp TestKernel64Bit
+    if (isAtLeastV && isKernel32Bit() && isAtLeastKernelVersion(5, 16, 0)) {
+        ALOGE("Android V+ platform with 32 bit kernel version >= 5.16.0 is unsupported");
+        if (!isTV()) return 1;
+    }
+
+    // Various known ABI layout issues, particularly wrt. bpf and ipsec/xfrm.
+    if (isAtLeastV && isKernel32Bit() && isX86()) {
         ALOGE("Android V requires X86 kernel to be 64-bit.");
         if (!isTV()) return 1;
     }
@@ -329,7 +372,7 @@
 
 #undef REQUIRE
 
-        if (bad && !isGSI()) {
+        if (bad) {
             ALOGE("Unsupported kernel version (%07x).", kernelVersion());
         }
     }
@@ -350,6 +393,10 @@
          * Some of these have userspace or kernel workarounds/hacks.
          * Some of them don't...
          * We're going to be removing the 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.
          *
          * Additionally the 32-bit kernel jit support is poor,
          * and 32-bit userspace on 64-bit kernel bpf ringbuffer compatibility is broken.
@@ -365,7 +412,9 @@
         return 1;
     }
 
-    if (isAtLeastV) {
+    if (runningAsRoot) {
+        // Note: writing this proc file requires being root (always the case on V+)
+
         // 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
@@ -375,6 +424,11 @@
     }
 
     if (isAtLeastU) {
+        // Note: writing these proc files requires CAP_NET_ADMIN
+        // and sepolicy which is only present on U+,
+        // on Android T and earlier versions they're written from the 'load_bpf_programs'
+        // trigger (ie. by init itself) instead.
+
         // 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
@@ -404,12 +458,6 @@
     // Thus we need to manually create the /sys/fs/bpf/loader subdirectory.
     if (createSysFsBpfSubDir("loader")) return 1;
 
-    // Version of Network BpfLoader depends on the Android OS version
-    unsigned int bpfloader_ver = 42u;  // [42] BPFLOADER_MAINLINE_VERSION
-    if (isAtLeastT) ++bpfloader_ver;   // [43] BPFLOADER_MAINLINE_T_VERSION
-    if (isAtLeastU) ++bpfloader_ver;   // [44] BPFLOADER_MAINLINE_U_VERSION
-    if (isAtLeastV) ++bpfloader_ver;   // [45] BPFLOADER_MAINLINE_V_VERSION
-
     // Load all ELF objects, create programs and maps, and pin them
     for (const auto& location : locations) {
         if (loadAllElfObjects(bpfloader_ver, location) != 0) {
@@ -432,17 +480,25 @@
         return 1;
     }
 
-    if (isAtLeastV) {
-        ALOGI("done, transferring control to platform bpfloader.");
+    // leave a flag that we're done
+    if (createSysFsBpfSubDir("netd_shared/mainline_done")) return 1;
 
-        const char * args[] = { platformBpfLoader, NULL, };
-        execve(args[0], (char**)args, envp);
-        ALOGE("FATAL: execve('%s'): %d[%s]", platformBpfLoader, errno, strerror(errno));
-        return 1;
+    // platform bpfloader will only succeed when run as root
+    if (!runningAsRoot) {
+        // unreachable on U QPR3+ which always runs netbpfload as root
+
+        ALOGI("mainline done, no need to transfer control to platform bpf loader.");
+        return 0;
     }
 
-    ALOGI("mainline done!");
-    return 0;
+    // unreachable before U QPR3
+    ALOGI("done, transferring control to platform bpfloader.");
+
+    // platform BpfLoader *needs* to run as root
+    const char * args[] = { platformBpfLoader, NULL, };
+    execve(args[0], (char**)args, envp);
+    ALOGE("FATAL: execve('%s'): %d[%s]", platformBpfLoader, errno, strerror(errno));
+    return 1;
 }
 
 }  // namespace bpf
diff --git a/netbpfload/loader.cpp b/netbpfload/loader.cpp
index 289b4d7..bb7c56f 100644
--- a/netbpfload/loader.cpp
+++ b/netbpfload/loader.cpp
@@ -165,32 +165,34 @@
  * 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},
+        {"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},
+        {"cgroupsockcreate/",  BPF_PROG_TYPE_CGROUP_SOCK,      BPF_CGROUP_INET_SOCK_CREATE},
+        {"cgroupsockrelease/", BPF_PROG_TYPE_CGROUP_SOCK,      BPF_CGROUP_INET_SOCK_RELEASE},
+        {"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 {
@@ -1122,11 +1124,20 @@
             readSectionUint("bpfloader_max_ver", elfFile, DEFAULT_BPFLOADER_MAX_VER);
     unsigned int bpfLoaderMinRequiredVer =
             readSectionUint("bpfloader_min_required_ver", elfFile, 0);
+    unsigned int netBpfLoadMinVer =
+            readSectionUint("netbpfload_min_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);
 
+    // temporary hack to enable gentle enablement of mainline NetBpfLoad
+    if (bpfloader_ver < netBpfLoadMinVer) {
+        ALOGI("NetBpfLoad version %d ignoring ELF object %s with netbpfload min ver %d",
+              bpfloader_ver, elfPath, netBpfLoadMinVer);
+        return 0;
+    }
+
     // inclusive lower bound check
     if (bpfloader_ver < bpfLoaderMinVer) {
         ALOGI("BpfLoader version 0x%05x ignoring ELF object %s with min ver 0x%05x",
diff --git a/netbpfload/netbpfload.33rc b/netbpfload/netbpfload.33rc
new file mode 100644
index 0000000..d269ce9
--- /dev/null
+++ b/netbpfload/netbpfload.33rc
@@ -0,0 +1,21 @@
+# This file takes effect only on T and U (on V netbpfload.35rc takes priority).
+#
+# The service is started from netd's libnetd_updatable shared library
+# on initial (boot time) startup of netd.
+#
+# However we never start this service on U QPR3.
+#
+# This is due to lack of a need: U QPR2 split the previously single
+# platform bpfloader into platform netbpfload -> platform bpfloader.
+# U QPR3 made the platform netbpfload unconditionally exec apex netbpfload,
+# so by the time U QPR3's netd runs, apex netbpfload is already done.
+
+service mdnsd_netbpfload /apex/com.android.tethering/bin/netbpfload
+    capabilities CHOWN SYS_ADMIN NET_ADMIN
+    group system root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw
+    user system
+    file /dev/kmsg w
+    rlimit memlock 1073741824 1073741824
+    oneshot
+    # TODO: reboot_on_failure reboot,netbpfload-failed
+    override
diff --git a/netbpfload/netbpfload.mainline.rc b/netbpfload/netbpfload.mainline.rc
deleted file mode 100644
index d38a503..0000000
--- a/netbpfload/netbpfload.mainline.rc
+++ /dev/null
@@ -1,17 +0,0 @@
-service mdnsd_loadbpf /system/bin/bpfloader
-    capabilities CHOWN SYS_ADMIN NET_ADMIN
-    group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
-    user root
-    rlimit memlock 1073741824 1073741824
-    oneshot
-    reboot_on_failure reboot,bpfloader-failed
-
-service bpfloader /apex/com.android.tethering/bin/netbpfload
-    capabilities CHOWN SYS_ADMIN NET_ADMIN
-    group system root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw
-    user system
-    file /dev/kmsg w
-    rlimit memlock 1073741824 1073741824
-    oneshot
-    reboot_on_failure reboot,bpfloader-failed
-    override
diff --git a/netd/BpfHandler.cpp b/netd/BpfHandler.cpp
index 91fec90..5d64ae7 100644
--- a/netd/BpfHandler.cpp
+++ b/netd/BpfHandler.cpp
@@ -34,6 +34,7 @@
 namespace net {
 
 using base::unique_fd;
+using base::WaitForProperty;
 using bpf::getSocketCookie;
 using bpf::retrieveProgram;
 using netdutils::Status;
@@ -85,31 +86,6 @@
         return Status("U+ platform with kernel version < 4.14.0 is unsupported");
     }
 
-    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)) {
-            return Status("V+ platform with kernel version < 4.19.0 is unsupported");
-        }
-
-        // 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)) {
-            return Status("V+ platform with 32 bit kernel, version >= 5.16.0 is unsupported");
-        }
-    }
-
-    // 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)) {
-        return Status("32 bit userspace with Kernel version >= 6.2.0 is unsupported");
-    }
-
     // U mandates this mount point (though it should also be the case on T)
     if (modules::sdklevel::IsAtLeastU() && !!strcmp(cg2_path, "/sys/fs/cgroup")) {
         return Status("U+ platform with cg2_path != /sys/fs/cgroup is unsupported");
@@ -134,8 +110,35 @@
     // TODO: delete the if statement once all devices should support cgroup
     // socket filter (ie. the minimum kernel version required is 4.14).
     if (bpf::isAtLeastKernelVersion(4, 14, 0)) {
-        RETURN_IF_NOT_OK(
-                attachProgramToCgroup(CGROUP_SOCKET_PROG_PATH, cg_fd, BPF_CGROUP_INET_SOCK_CREATE));
+        RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_INET_CREATE_PROG_PATH,
+                                    cg_fd, BPF_CGROUP_INET_SOCK_CREATE));
+    }
+
+    if (modules::sdklevel::IsAtLeastV()) {
+        RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_CONNECT4_PROG_PATH,
+                                    cg_fd, BPF_CGROUP_INET4_CONNECT));
+        RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_CONNECT6_PROG_PATH,
+                                    cg_fd, BPF_CGROUP_INET6_CONNECT));
+        RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_UDP4_RECVMSG_PROG_PATH,
+                                    cg_fd, BPF_CGROUP_UDP4_RECVMSG));
+        RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_UDP6_RECVMSG_PROG_PATH,
+                                    cg_fd, BPF_CGROUP_UDP6_RECVMSG));
+        RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_UDP4_SENDMSG_PROG_PATH,
+                                    cg_fd, BPF_CGROUP_UDP4_SENDMSG));
+        RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_UDP6_SENDMSG_PROG_PATH,
+                                    cg_fd, BPF_CGROUP_UDP6_SENDMSG));
+
+        if (bpf::isAtLeastKernelVersion(5, 4, 0)) {
+            RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_GETSOCKOPT_PROG_PATH,
+                                        cg_fd, BPF_CGROUP_GETSOCKOPT));
+            RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_SETSOCKOPT_PROG_PATH,
+                                        cg_fd, BPF_CGROUP_SETSOCKOPT));
+        }
+
+        if (bpf::isAtLeastKernelVersion(5, 10, 0)) {
+            RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_INET_RELEASE_PROG_PATH,
+                                        cg_fd, BPF_CGROUP_INET_SOCK_RELEASE));
+        }
     }
 
     if (bpf::isAtLeastKernelVersion(4, 19, 0)) {
@@ -155,6 +158,24 @@
         if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET6_BIND) <= 0) abort();
     }
 
+    if (modules::sdklevel::IsAtLeastV()) {
+        if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET4_CONNECT) <= 0) abort();
+        if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET6_CONNECT) <= 0) abort();
+        if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP4_RECVMSG) <= 0) abort();
+        if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP6_RECVMSG) <= 0) abort();
+        if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP4_SENDMSG) <= 0) abort();
+        if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP6_SENDMSG) <= 0) abort();
+
+        if (bpf::isAtLeastKernelVersion(5, 4, 0)) {
+            if (bpf::queryProgram(cg_fd, BPF_CGROUP_GETSOCKOPT) <= 0) abort();
+            if (bpf::queryProgram(cg_fd, BPF_CGROUP_SETSOCKOPT) <= 0) abort();
+        }
+
+        if (bpf::isAtLeastKernelVersion(5, 10, 0)) {
+            if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_SOCK_RELEASE) <= 0) abort();
+        }
+    }
+
     return netdutils::status::ok;
 }
 
@@ -165,39 +186,56 @@
 BpfHandler::BpfHandler(uint32_t perUidLimit, uint32_t totalLimit)
     : mPerUidStatsEntriesLimit(perUidLimit), mTotalUidStatsEntriesLimit(totalLimit) {}
 
+static bool mainlineNetBpfLoadDone() {
+    return !access("/sys/fs/bpf/netd_shared/mainline_done", F_OK);
+}
+
 // copied with minor changes from waitForProgsLoaded()
 // p/m/C's staticlibs/native/bpf_headers/include/bpf/WaitForProgsLoaded.h
 static inline void waitForNetProgsLoaded() {
     // infinite loop until success with 5/10/20/40/60/60/60... delay
     for (int delay = 5;; delay *= 2) {
         if (delay > 60) delay = 60;
-        if (base::WaitForProperty("init.svc.bpfloader", "stopped", std::chrono::seconds(delay))
-            && !access("/sys/fs/bpf/netd_shared", F_OK))
+        if (WaitForProperty("init.svc.mdnsd_netbpfload", "stopped", std::chrono::seconds(delay))
+            && mainlineNetBpfLoadDone())
             return;
-        ALOGW("Waited %ds for init.svc.bpfloader=stopped, still waiting...", delay);
+        ALOGW("Waited %ds for init.svc.mdnsd_netbpfload=stopped, still waiting...", delay);
     }
 }
 
 Status BpfHandler::init(const char* cg2_path) {
+    // Note: netd *can* be restarted, so this might get called a second time after boot is complete
+    // at which point we don't need to (and shouldn't) wait for (more importantly start) loading bpf
+
     if (base::GetProperty("bpf.progs_loaded", "") != "1") {
-        // Make sure BPF programs are loaded before doing anything
-        ALOGI("Waiting for BPF programs");
-
-        // TODO: use !modules::sdklevel::IsAtLeastV() once api finalized
-        if (android_get_device_api_level() < __ANDROID_API_V__) {
-            waitForNetProgsLoaded();
-            ALOGI("Networking BPF programs are loaded");
-
-            if (!base::SetProperty("ctl.start", "mdnsd_loadbpf")) {
-                ALOGE("Failed to set property ctl.start=mdnsd_loadbpf, see dmesg for reason.");
-                abort();
-            }
-
-            ALOGI("Waiting for remaining BPF programs");
-        }
-
+        // AOSP platform netd & mainline don't need this (at least prior to U QPR3),
+        // but there could be platform provided (xt_)bpf programs that oem/vendor
+        // modified netd (which calls us during init) depends on...
+        ALOGI("Waiting for platform BPF programs");
         android::bpf::waitForProgsLoaded();
     }
+
+    if (!mainlineNetBpfLoadDone()) {
+        const bool enforce_mainline = false; // TODO: flip to true
+
+        // We're on < U QPR3 & it's the first time netd is starting up (unless crashlooping)
+        //
+        // On U QPR3+ netbpfload is guaranteed to run before the platform bpfloader,
+        // so waitForProgsLoaded() implies mainlineNetBpfLoadDone().
+        if (!base::SetProperty("ctl.start", "mdnsd_netbpfload")) {
+            ALOGE("Failed to set property ctl.start=mdnsd_netbpfload, see dmesg for reason.");
+            if (enforce_mainline) abort();
+        }
+
+        if (enforce_mainline) {
+            ALOGI("Waiting for Networking BPF programs");
+            waitForNetProgsLoaded();
+            ALOGI("Networking BPF programs are loaded");
+        } else {
+            ALOGI("Started mdnsd_netbpfload asynchronously.");
+        }
+    }
+
     ALOGI("BPF programs are loaded");
 
     RETURN_IF_NOT_OK(initPrograms(cg2_path));
@@ -206,7 +244,30 @@
     return netdutils::status::ok;
 }
 
+static void mapLockTest(void) {
+    // The maps must be R/W, and as yet unopened (or more specifically not yet lock'ed).
+    const char * const m1 = BPF_NETD_PATH "map_netd_lock_array_test_map";
+    const char * const m2 = BPF_NETD_PATH "map_netd_lock_hash_test_map";
+
+    unique_fd fd0(bpf::mapRetrieveExclusiveRW(m1)); if (!fd0.ok()) abort();  // grabs exclusive lock
+
+    unique_fd fd1(bpf::mapRetrieveExclusiveRW(m2)); if (!fd1.ok()) abort();  // no conflict with fd0
+    unique_fd fd2(bpf::mapRetrieveExclusiveRW(m2)); if ( fd2.ok()) abort();  // busy due to fd1
+    unique_fd fd3(bpf::mapRetrieveRO(m2));          if (!fd3.ok()) abort();  // no lock taken
+    unique_fd fd4(bpf::mapRetrieveRW(m2));          if ( fd4.ok()) abort();  // busy due to fd1
+    fd1.reset();  // releases exclusive lock
+    unique_fd fd5(bpf::mapRetrieveRO(m2));          if (!fd5.ok()) abort();  // no lock taken
+    unique_fd fd6(bpf::mapRetrieveRW(m2));          if (!fd6.ok()) abort();  // now ok
+    unique_fd fd7(bpf::mapRetrieveRO(m2));          if (!fd7.ok()) abort();  // no lock taken
+    unique_fd fd8(bpf::mapRetrieveExclusiveRW(m2)); if ( fd8.ok()) abort();  // busy due to fd6
+
+    fd0.reset();  // releases exclusive lock
+    unique_fd fd9(bpf::mapRetrieveWO(m1));          if (!fd9.ok()) abort();  // grabs exclusive lock
+}
+
 Status BpfHandler::initMaps() {
+    mapLockTest();
+
     RETURN_IF_NOT_OK(mStatsMapA.init(STATS_MAP_A_PATH));
     RETURN_IF_NOT_OK(mStatsMapB.init(STATS_MAP_B_PATH));
     RETURN_IF_NOT_OK(mConfigurationMap.init(CONFIGURATION_MAP_PATH));
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index f8b0d53..64624ae 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -1923,13 +1923,13 @@
                         mContext, MdnsFeatureFlags.NSD_FORCE_DISABLE_MDNS_OFFLOAD))
                 .setIncludeInetAddressRecordsInProbing(mDeps.isFeatureEnabled(
                         mContext, MdnsFeatureFlags.INCLUDE_INET_ADDRESS_RECORDS_IN_PROBING))
-                .setIsExpiredServicesRemovalEnabled(mDeps.isFeatureEnabled(
+                .setIsExpiredServicesRemovalEnabled(mDeps.isTetheringFeatureNotChickenedOut(
                         mContext, MdnsFeatureFlags.NSD_EXPIRED_SERVICES_REMOVAL))
                 .setIsLabelCountLimitEnabled(mDeps.isTetheringFeatureNotChickenedOut(
                         mContext, MdnsFeatureFlags.NSD_LIMIT_LABEL_COUNT))
-                .setIsKnownAnswerSuppressionEnabled(mDeps.isFeatureEnabled(
+                .setIsKnownAnswerSuppressionEnabled(mDeps.isTetheringFeatureNotChickenedOut(
                         mContext, MdnsFeatureFlags.NSD_KNOWN_ANSWER_SUPPRESSION))
-                .setIsUnicastReplyEnabled(mDeps.isFeatureEnabled(
+                .setIsUnicastReplyEnabled(mDeps.isTetheringFeatureNotChickenedOut(
                         mContext, MdnsFeatureFlags.NSD_UNICAST_REPLY_ENABLED))
                 .setIsAggressiveQueryModeEnabled(mDeps.isFeatureEnabled(
                         mContext, MdnsFeatureFlags.NSD_AGGRESSIVE_QUERY_MODE))
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
index f4a08ba..c264f25 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
@@ -189,10 +189,10 @@
         public Builder() {
             mIsMdnsOffloadFeatureEnabled = false;
             mIncludeInetAddressRecordsInProbing = false;
-            mIsExpiredServicesRemovalEnabled = false;
+            mIsExpiredServicesRemovalEnabled = true; // Default enabled.
             mIsLabelCountLimitEnabled = true; // Default enabled.
-            mIsKnownAnswerSuppressionEnabled = false;
-            mIsUnicastReplyEnabled = true;
+            mIsKnownAnswerSuppressionEnabled = true; // Default enabled.
+            mIsUnicastReplyEnabled = true; // Default enabled.
             mIsAggressiveQueryModeEnabled = false;
             mIsQueryWithKnownAnswerEnabled = false;
             mOverrideProvider = null;
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 8305c1e..114cf2e 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -57,6 +57,7 @@
 import static android.net.TrafficStats.TYPE_TX_PACKETS;
 import static android.net.TrafficStats.UID_TETHERING;
 import static android.net.TrafficStats.UNSUPPORTED;
+import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE;
 import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID;
 import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID_TAG;
 import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_XT;
@@ -91,6 +92,7 @@
 import android.app.AlarmManager;
 import android.app.BroadcastOptions;
 import android.app.PendingIntent;
+import android.app.compat.CompatChanges;
 import android.app.usage.NetworkStatsManager;
 import android.content.ApexEnvironment;
 import android.content.BroadcastReceiver;
@@ -472,7 +474,9 @@
     private final TrafficStatsRateLimitCache mTrafficStatsUidCache;
     static final String TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG =
             "trafficstats_rate_limit_cache_enabled_flag";
-    private final boolean mSupportTrafficStatsRateLimitCache;
+    private final boolean mAlwaysUseTrafficStatsRateLimitCache;
+    private final int mTrafficStatsRateLimitCacheExpiryDuration;
+    private final int mTrafficStatsRateLimitCacheMaxEntries;
 
     private final Object mOpenSessionCallsLock = new Object();
 
@@ -663,15 +667,18 @@
             mEventLogger = null;
         }
 
-        final long cacheExpiryDurationMs = mDeps.getTrafficStatsRateLimitCacheExpiryDuration();
-        final int cacheMaxEntries = mDeps.getTrafficStatsRateLimitCacheMaxEntries();
-        mSupportTrafficStatsRateLimitCache = mDeps.supportTrafficStatsRateLimitCache(mContext);
+        mAlwaysUseTrafficStatsRateLimitCache =
+                mDeps.alwaysUseTrafficStatsRateLimitCache(mContext);
+        mTrafficStatsRateLimitCacheExpiryDuration =
+                mDeps.getTrafficStatsRateLimitCacheExpiryDuration();
+        mTrafficStatsRateLimitCacheMaxEntries =
+                mDeps.getTrafficStatsRateLimitCacheMaxEntries();
         mTrafficStatsTotalCache = new TrafficStatsRateLimitCache(mClock,
-                cacheExpiryDurationMs, cacheMaxEntries);
+                mTrafficStatsRateLimitCacheExpiryDuration, mTrafficStatsRateLimitCacheMaxEntries);
         mTrafficStatsIfaceCache = new TrafficStatsRateLimitCache(mClock,
-                cacheExpiryDurationMs, cacheMaxEntries);
+                mTrafficStatsRateLimitCacheExpiryDuration, mTrafficStatsRateLimitCacheMaxEntries);
         mTrafficStatsUidCache = new TrafficStatsRateLimitCache(mClock,
-                cacheExpiryDurationMs, cacheMaxEntries);
+                mTrafficStatsRateLimitCacheExpiryDuration, mTrafficStatsRateLimitCacheMaxEntries);
 
         // TODO: Remove bpfNetMaps creation and always start SkDestroyListener
         // Following code is for the experiment to verify the SkDestroyListener refactoring. Based
@@ -920,13 +927,14 @@
         }
 
         /**
-         * Get whether TrafficStats rate-limit cache is supported.
+         * Get whether TrafficStats rate-limit cache is always applied.
          *
          * This method should only be called once in the constructor,
          * to ensure that the code does not need to deal with flag values changing at runtime.
          */
-        public boolean supportTrafficStatsRateLimitCache(@NonNull Context ctx) {
-            return false;
+        public boolean alwaysUseTrafficStatsRateLimitCache(@NonNull Context ctx) {
+            return SdkLevel.isAtLeastV() && DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
+                    ctx, TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG);
         }
 
         /**
@@ -954,6 +962,13 @@
         }
 
         /**
+         * Wrapper method for {@link CompatChanges#isChangeEnabled(long, int)}
+         */
+        public boolean isChangeEnabled(final long changeId, final int uid) {
+            return CompatChanges.isChangeEnabled(changeId, uid);
+        }
+
+        /**
          * Retrieves native network total statistics.
          *
          * @return A NetworkStats.Entry containing the native statistics, or
@@ -1529,7 +1544,11 @@
     }
 
     @Override
-    public INetworkStatsSession openSessionForUsageStats(int flags, String callingPackage) {
+    public INetworkStatsSession openSessionForUsageStats(
+            int flags, @NonNull String callingPackage) {
+        Objects.requireNonNull(callingPackage);
+        PermissionUtils.enforcePackageNameMatchesUid(
+                mContext, Binder.getCallingUid(), callingPackage);
         return openSessionInternal(flags, callingPackage);
     }
 
@@ -2046,6 +2065,7 @@
 
         final int callingPid = Binder.getCallingPid();
         final int callingUid = Binder.getCallingUid();
+        PermissionUtils.enforcePackageNameMatchesUid(mContext, callingUid, callingPackage);
         @NetworkStatsAccess.Level int accessLevel = checkAccessLevel(callingPackage);
         DataUsageRequest normalizedRequest;
         final long token = Binder.clearCallingIdentity();
@@ -2084,14 +2104,14 @@
         }
         if (!isEntryValueTypeValid(type)) return UNSUPPORTED;
 
-        if (!mSupportTrafficStatsRateLimitCache) {
-            return getEntryValueForType(mDeps.nativeGetUidStat(uid), type);
+        if (mAlwaysUseTrafficStatsRateLimitCache
+                || mDeps.isChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, callingUid)) {
+            final NetworkStats.Entry entry = mTrafficStatsUidCache.getOrCompute(IFACE_ALL, uid,
+                    () -> mDeps.nativeGetUidStat(uid));
+            return getEntryValueForType(entry, type);
         }
 
-        final NetworkStats.Entry entry = mTrafficStatsUidCache.getOrCompute(IFACE_ALL, uid,
-                () -> mDeps.nativeGetUidStat(uid));
-
-        return getEntryValueForType(entry, type);
+        return getEntryValueForType(mDeps.nativeGetUidStat(uid), type);
     }
 
     @Nullable
@@ -2113,14 +2133,15 @@
         Objects.requireNonNull(iface);
         if (!isEntryValueTypeValid(type)) return UNSUPPORTED;
 
-        if (!mSupportTrafficStatsRateLimitCache) {
-            return getEntryValueForType(getIfaceStatsInternal(iface), type);
+        if (mAlwaysUseTrafficStatsRateLimitCache
+                || mDeps.isChangeEnabled(
+                        ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, Binder.getCallingUid())) {
+            final NetworkStats.Entry entry = mTrafficStatsIfaceCache.getOrCompute(iface, UID_ALL,
+                    () -> getIfaceStatsInternal(iface));
+            return getEntryValueForType(entry, type);
         }
 
-        final NetworkStats.Entry entry = mTrafficStatsIfaceCache.getOrCompute(iface, UID_ALL,
-                () -> getIfaceStatsInternal(iface));
-
-        return getEntryValueForType(entry, type);
+        return getEntryValueForType(getIfaceStatsInternal(iface), type);
     }
 
     private long getEntryValueForType(@Nullable NetworkStats.Entry entry, int type) {
@@ -2166,14 +2187,15 @@
     @Override
     public long getTotalStats(int type) {
         if (!isEntryValueTypeValid(type)) return UNSUPPORTED;
-        if (!mSupportTrafficStatsRateLimitCache) {
-            return getEntryValueForType(getTotalStatsInternal(), type);
+        if (mAlwaysUseTrafficStatsRateLimitCache
+                || mDeps.isChangeEnabled(
+                        ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, Binder.getCallingUid())) {
+            final NetworkStats.Entry entry = mTrafficStatsTotalCache.getOrCompute(
+                    IFACE_ALL, UID_ALL, () -> getTotalStatsInternal());
+            return getEntryValueForType(entry, type);
         }
 
-        final NetworkStats.Entry entry = mTrafficStatsTotalCache.getOrCompute(IFACE_ALL, UID_ALL,
-                () -> getTotalStatsInternal());
-
-        return getEntryValueForType(entry, type);
+        return getEntryValueForType(getTotalStatsInternal(), type);
     }
 
     @Override
@@ -2937,13 +2959,12 @@
             } catch (IOException e) {
                 pw.println("(failed to dump FastDataInput counters)");
             }
-            pw.print("trafficstats.cache.supported", mSupportTrafficStatsRateLimitCache);
+            pw.print("trafficstats.cache.alwaysuse", mAlwaysUseTrafficStatsRateLimitCache);
             pw.println();
             pw.print(TRAFFIC_STATS_CACHE_EXPIRY_DURATION_NAME,
-                    mDeps.getTrafficStatsRateLimitCacheExpiryDuration());
+                    mTrafficStatsRateLimitCacheExpiryDuration);
             pw.println();
-            pw.print(TRAFFIC_STATS_CACHE_MAX_ENTRIES_NAME,
-                    mDeps.getTrafficStatsRateLimitCacheMaxEntries());
+            pw.print(TRAFFIC_STATS_CACHE_MAX_ENTRIES_NAME, mTrafficStatsRateLimitCacheMaxEntries);
             pw.println();
 
             pw.decreaseIndent();
diff --git a/service/ServiceConnectivityResources/res/values-de/strings.xml b/service/ServiceConnectivityResources/res/values-de/strings.xml
index 536ebda..f58efb0 100644
--- a/service/ServiceConnectivityResources/res/values-de/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-de/strings.xml
@@ -29,7 +29,7 @@
     <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Für Optionen tippen"</string>
     <string name="mobile_no_internet" msgid="4087718456753201450">"Mobiles Netzwerk hat keinen Internetzugriff"</string>
     <string name="other_networks_no_internet" msgid="5693932964749676542">"Netzwerk hat keinen Internetzugriff"</string>
-    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Auf den privaten DNS-Server kann nicht zugegriffen werden"</string>
+    <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Auf den Server des privaten DNS kann nicht zugegriffen werden"</string>
     <string name="network_partial_connectivity" msgid="5549503845834993258">"Schlechte Verbindung mit <xliff:g id="NETWORK_SSID">%1$s</xliff:g>"</string>
     <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Tippen, um die Verbindung trotzdem herzustellen"</string>
     <string name="network_switch_metered" msgid="5016937523571166319">"Zu <xliff:g id="NETWORK_TYPE">%1$s</xliff:g> gewechselt"</string>
diff --git a/service/ServiceConnectivityResources/res/values/config_thread.xml b/service/ServiceConnectivityResources/res/values/config_thread.xml
index 4783f2b..02a9ce6 100644
--- a/service/ServiceConnectivityResources/res/values/config_thread.xml
+++ b/service/ServiceConnectivityResources/res/values/config_thread.xml
@@ -53,4 +53,10 @@
     UTF-8 bytes.
     -->
     <string translatable="false" name="config_thread_model_name">Thread Border Router</string>
+
+    <!-- Whether the Thread network will be managed by the Google Home ecosystem. When this value
+    is set, a TXT entry "vgh=0" or "vgh=1" will be added to the "_mehscop._udp" mDNS service
+    respectively (The TXT value is a string).
+    -->
+    <bool name="config_thread_managed_by_google_home">false</bool>
 </resources>
diff --git a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
index c07d050..c0082bb 100644
--- a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
+++ b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
@@ -80,7 +80,8 @@
       case VERIFY_BIN: return;
       case VERIFY_PROG:   fd = bpf::retrieveProgram(path); break;
       case VERIFY_MAP_RO: fd = bpf::mapRetrieveRO(path); break;
-      case VERIFY_MAP_RW: fd = bpf::mapRetrieveRW(path); break;
+      // lockless: we're just checking access rights and will immediately close the fd
+      case VERIFY_MAP_RW: fd = bpf::mapRetrieveLocklessRW(path); break;
     }
 
     if (fd < 0) ALOGF("bpf_obj_get '%s' failed, errno=%d", path, errno);
@@ -114,12 +115,7 @@
 
     V("/sys/fs/bpf", S_IFDIR|S_ISVTX|0777, ROOT, ROOT, "fs_bpf", DIR);
 
-    // TODO: use modules::sdklevel::IsAtLeastV() once api finalized
-    if (android_get_device_api_level() >= __ANDROID_API_V__) {
-        V("/sys/fs/bpf/net_shared", S_IFDIR|01777, ROOT, ROOT, "fs_bpf_net_shared", DIR);
-    } else {
-        V("/sys/fs/bpf/net_shared", S_IFDIR|01777, SYSTEM, SYSTEM, "fs_bpf_net_shared", DIR);
-    }
+    V("/sys/fs/bpf/net_shared", S_IFDIR|01777, ROOT, ROOT, "fs_bpf_net_shared", DIR);
 
     // pre-U we do not have selinux privs to getattr on bpf maps/progs
     // so while the below *should* be as listed, we have no way to actually verify
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index 1047232..b3e7d8c 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -72,6 +72,7 @@
 import com.android.net.module.util.BpfDump;
 import com.android.net.module.util.BpfMap;
 import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.SingleWriterBpfMap;
 import com.android.net.module.util.Struct;
 import com.android.net.module.util.Struct.S32;
 import com.android.net.module.util.Struct.U32;
@@ -188,7 +189,7 @@
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private static IBpfMap<S32, U32> getConfigurationMap() {
         try {
-            return new BpfMap<>(
+            return SingleWriterBpfMap.getSingleton(
                     CONFIGURATION_MAP_PATH, S32.class, U32.class);
         } catch (ErrnoException e) {
             throw new IllegalStateException("Cannot open netd configuration map", e);
@@ -198,7 +199,7 @@
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private static IBpfMap<S32, UidOwnerValue> getUidOwnerMap() {
         try {
-            return new BpfMap<>(
+            return SingleWriterBpfMap.getSingleton(
                     UID_OWNER_MAP_PATH, S32.class, UidOwnerValue.class);
         } catch (ErrnoException e) {
             throw new IllegalStateException("Cannot open uid owner map", e);
@@ -208,7 +209,7 @@
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private static IBpfMap<S32, U8> getUidPermissionMap() {
         try {
-            return new BpfMap<>(
+            return SingleWriterBpfMap.getSingleton(
                     UID_PERMISSION_MAP_PATH, S32.class, U8.class);
         } catch (ErrnoException e) {
             throw new IllegalStateException("Cannot open uid permission map", e);
@@ -218,6 +219,7 @@
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private static IBpfMap<CookieTagMapKey, CookieTagMapValue> getCookieTagMap() {
         try {
+            // Cannot use SingleWriterBpfMap because it's written by ClatCoordinator as well.
             return new BpfMap<>(COOKIE_TAG_MAP_PATH,
                     CookieTagMapKey.class, CookieTagMapValue.class);
         } catch (ErrnoException e) {
@@ -228,7 +230,7 @@
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private static IBpfMap<S32, U8> getDataSaverEnabledMap() {
         try {
-            return new BpfMap<>(
+            return SingleWriterBpfMap.getSingleton(
                     DATA_SAVER_ENABLED_MAP_PATH, S32.class, U8.class);
         } catch (ErrnoException e) {
             throw new IllegalStateException("Cannot open data saver enabled map", e);
@@ -238,7 +240,7 @@
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private static IBpfMap<IngressDiscardKey, IngressDiscardValue> getIngressDiscardMap() {
         try {
-            return new BpfMap<>(INGRESS_DISCARD_MAP_PATH,
+            return SingleWriterBpfMap.getSingleton(INGRESS_DISCARD_MAP_PATH,
                     IngressDiscardKey.class, IngressDiscardValue.class);
         } catch (ErrnoException e) {
             throw new IllegalStateException("Cannot open ingress discard map", e);
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index bb77a9d..be1d3c7 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -77,6 +77,7 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
@@ -108,14 +109,24 @@
 import static android.os.Process.INVALID_UID;
 import static android.os.Process.VPN_UID;
 import static android.system.OsConstants.ETH_P_ALL;
+import static android.system.OsConstants.F_OK;
 import static android.system.OsConstants.IPPROTO_TCP;
 import static android.system.OsConstants.IPPROTO_UDP;
 
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_GETSOCKOPT;
 import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET4_BIND;
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET4_CONNECT;
 import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET6_BIND;
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET6_CONNECT;
 import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET_EGRESS;
 import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET_INGRESS;
 import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET_SOCK_CREATE;
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET_SOCK_RELEASE;
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_SETSOCKOPT;
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_UDP4_RECVMSG;
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_UDP4_SENDMSG;
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_UDP6_RECVMSG;
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_UDP6_SENDMSG;
 import static com.android.net.module.util.NetworkMonitorUtils.isPrivateDnsValidationRequired;
 import static com.android.net.module.util.PermissionUtils.enforceAnyPermissionOf;
 import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermission;
@@ -132,6 +143,7 @@
 import android.annotation.CheckResult;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresApi;
 import android.annotation.SuppressLint;
 import android.annotation.TargetApi;
 import android.app.ActivityManager;
@@ -267,6 +279,7 @@
 import android.stats.connectivity.ValidatedState;
 import android.sysprop.NetworkProperties;
 import android.system.ErrnoException;
+import android.system.Os;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
@@ -280,8 +293,6 @@
 import android.util.SparseIntArray;
 import android.util.StatsEvent;
 
-import androidx.annotation.RequiresApi;
-
 import com.android.connectivity.resources.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -382,6 +393,7 @@
 import java.util.Map.Entry;
 import java.util.NoSuchElementException;
 import java.util.Objects;
+import java.util.Random;
 import java.util.Set;
 import java.util.SortedSet;
 import java.util.StringJoiner;
@@ -489,6 +501,8 @@
     private final boolean mRequestRestrictedWifiEnabled;
     private final boolean mBackgroundFirewallChainEnabled;
 
+    private final boolean mUseDeclaredMethodsForCallbacksEnabled;
+
     /**
      * Uids ConnectivityService tracks blocked status of to send blocked status callbacks.
      * Key is uid based on mAsUid of registered networkRequestInfo
@@ -1831,6 +1845,33 @@
                 new PermissionMonitor(mContext, mNetd, mBpfNetMaps, mHandlerThread);
         mHandlerThread.start();
         mHandler = new InternalHandler(mHandlerThread.getLooper());
+        // Temporary hack to report netbpfload result.
+        // TODO: remove in 2024-09 when netbpfload starts loading mainline bpf programs.
+        mHandler.postDelayed(() -> {
+            // Test Pitot pipeline, ignore this Log.wtf if it shows up in the logs.
+            final Random r = new Random();
+            if (Build.TYPE.equals("user") && r.nextInt(1000) == 0) {
+                Log.wtf(TAG, "NOT A FAILURE, PLEASE IGNORE! Test Pitot pipeline works correctly");
+            }
+            // Did netbpfload create the map?
+            try {
+                Os.access("/sys/fs/bpf/net_shared/map_gentle_test", F_OK);
+            } catch (ErrnoException e) {
+                Log.wtf(TAG, "netbpfload did not create map", e);
+            }
+            // Did netbpfload create the program?
+            try {
+                Os.access("/sys/fs/bpf/net_shared/prog_gentle_skfilter_accept", F_OK);
+            } catch (ErrnoException e) {
+                Log.wtf(TAG, "netbpfload did not create program", e);
+            }
+            // Did netbpfload run to completion?
+            try {
+                Os.access("/sys/fs/bpf/netd_shared/mainline_done", F_OK);
+            } catch (ErrnoException e) {
+                Log.wtf(TAG, "netbpfload did not run to completion", e);
+            }
+        }, 30_000 /* delayMillis */);
         mTrackerHandler = new NetworkStateTrackerHandler(mHandlerThread.getLooper());
         mConnectivityDiagnosticsHandler =
                 new ConnectivityDiagnosticsHandler(mHandlerThread.getLooper());
@@ -1850,6 +1891,8 @@
                 && mDeps.isFeatureEnabled(context, REQUEST_RESTRICTED_WIFI);
         mBackgroundFirewallChainEnabled = mDeps.isAtLeastV() && mDeps.isFeatureNotChickenedOut(
                 context, ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN);
+        mUseDeclaredMethodsForCallbacksEnabled = mDeps.isFeatureEnabled(context,
+                ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS);
         mCarrierPrivilegeAuthenticator = mDeps.makeCarrierPrivilegeAuthenticator(
                 mContext, mTelephonyManager, mRequestRestrictedWifiEnabled,
                 this::handleUidCarrierPrivilegesLost, mHandler);
@@ -3561,6 +3604,7 @@
         pw.decreaseIndent();
     }
 
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private void dumpBpfProgramStatus(IndentingPrintWriter pw) {
         pw.println("Bpf Program Status:");
         pw.increaseIndent();
@@ -3569,12 +3613,37 @@
             pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET_INGRESS));
             pw.print("CGROUP_INET_EGRESS: ");
             pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET_EGRESS));
+
             pw.print("CGROUP_INET_SOCK_CREATE: ");
             pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET_SOCK_CREATE));
+
             pw.print("CGROUP_INET4_BIND: ");
             pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET4_BIND));
             pw.print("CGROUP_INET6_BIND: ");
             pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET6_BIND));
+
+            pw.print("CGROUP_INET4_CONNECT: ");
+            pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET4_CONNECT));
+            pw.print("CGROUP_INET6_CONNECT: ");
+            pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET6_CONNECT));
+
+            pw.print("CGROUP_UDP4_SENDMSG: ");
+            pw.println(mDeps.getBpfProgramId(BPF_CGROUP_UDP4_SENDMSG));
+            pw.print("CGROUP_UDP6_SENDMSG: ");
+            pw.println(mDeps.getBpfProgramId(BPF_CGROUP_UDP6_SENDMSG));
+
+            pw.print("CGROUP_UDP4_RECVMSG: ");
+            pw.println(mDeps.getBpfProgramId(BPF_CGROUP_UDP4_RECVMSG));
+            pw.print("CGROUP_UDP6_RECVMSG: ");
+            pw.println(mDeps.getBpfProgramId(BPF_CGROUP_UDP6_RECVMSG));
+
+            pw.print("CGROUP_GETSOCKOPT: ");
+            pw.println(mDeps.getBpfProgramId(BPF_CGROUP_GETSOCKOPT));
+            pw.print("CGROUP_SETSOCKOPT: ");
+            pw.println(mDeps.getBpfProgramId(BPF_CGROUP_SETSOCKOPT));
+
+            pw.print("CGROUP_INET_SOCK_RELEASE: ");
+            pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET_SOCK_RELEASE));
         } catch (IOException e) {
             pw.println("  IOException");
         }
@@ -4224,8 +4293,19 @@
         pw.println();
         dumpDestroySockets(pw);
 
-        pw.println();
-        dumpBpfProgramStatus(pw);
+        if (mDeps.isAtLeastT()) {
+            // R: https://android.googlesource.com/platform/system/core/+/refs/heads/android11-release/rootdir/init.rc
+            //   shows /dev/cg2_bpf
+            // S: https://android.googlesource.com/platform/system/core/+/refs/heads/android12-release/rootdir/init.rc
+            //   does not
+            // Thus cgroups are mounted at /dev/cg2_bpf on R and not on /sys/fs/cgroup
+            // so the following won't work (on R) anyway.
+            // The /sys/fs/cgroup path is only actually enforced/required starting with U,
+            // but it is very likely to already be the case (though not guaranteed) on T.
+            // I'm not at all sure about S - let's just skip it to get rid of lint warnings.
+            pw.println();
+            dumpBpfProgramStatus(pw);
+        }
 
         if (null != mCarrierPrivilegeAuthenticator) {
             pw.println();
@@ -13282,11 +13362,12 @@
         requests.add(createDefaultInternetRequestForTransport(
                 TYPE_NONE, NetworkRequest.Type.TRACK_DEFAULT));
 
-        // request: restricted Satellite internet
+        // request: Satellite internet, satellite network could be restricted or constrained
         final NetworkCapabilities cap = new NetworkCapabilities.Builder()
                 .addCapability(NET_CAPABILITY_INTERNET)
                 .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
                 .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                .removeCapability(NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
                 .addTransportType(NetworkCapabilities.TRANSPORT_SATELLITE)
                 .build();
         requests.add(createNetworkRequest(NetworkRequest.Type.REQUEST, cap));
@@ -14045,4 +14126,16 @@
         enforceNetworkStackPermission(mContext);
         return mRoutingCoordinatorService;
     }
+
+    @Override
+    public long getEnabledConnectivityManagerFeatures() {
+        long features = 0;
+        // The bitmask must be built based on final properties initialized in the constructor, to
+        // ensure that it does not change over time and is always consistent between
+        // ConnectivityManager and ConnectivityService.
+        if (mUseDeclaredMethodsForCallbacksEnabled) {
+            features |= ConnectivityManager.FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS;
+        }
+        return features;
+    }
 }
diff --git a/service/src/com/android/server/TestNetworkService.java b/service/src/com/android/server/TestNetworkService.java
index 843b7b3..4d39d7d 100644
--- a/service/src/com/android/server/TestNetworkService.java
+++ b/service/src/com/android/server/TestNetworkService.java
@@ -267,6 +267,7 @@
         nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
         nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
         nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN);
+        nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED);
         nc.setNetworkSpecifier(new TestNetworkSpecifier(iface));
         nc.setAdministratorUids(administratorUids);
         if (!isMetered) {
diff --git a/service/src/com/android/server/connectivity/ClatCoordinator.java b/service/src/com/android/server/connectivity/ClatCoordinator.java
index d886182..b1c770b 100644
--- a/service/src/com/android/server/connectivity/ClatCoordinator.java
+++ b/service/src/com/android/server/connectivity/ClatCoordinator.java
@@ -45,6 +45,7 @@
 import com.android.net.module.util.BpfMap;
 import com.android.net.module.util.IBpfMap;
 import com.android.net.module.util.InterfaceParams;
+import com.android.net.module.util.SingleWriterBpfMap;
 import com.android.net.module.util.TcUtils;
 import com.android.net.module.util.bpf.ClatEgress4Key;
 import com.android.net.module.util.bpf.ClatEgress4Value;
@@ -256,7 +257,7 @@
         @Nullable
         public IBpfMap<ClatIngress6Key, ClatIngress6Value> getBpfIngress6Map() {
             try {
-                return new BpfMap<>(CLAT_INGRESS6_MAP_PATH,
+                return SingleWriterBpfMap.getSingleton(CLAT_INGRESS6_MAP_PATH,
                        ClatIngress6Key.class, ClatIngress6Value.class);
             } catch (ErrnoException e) {
                 Log.e(TAG, "Cannot create ingress6 map: " + e);
@@ -268,7 +269,7 @@
         @Nullable
         public IBpfMap<ClatEgress4Key, ClatEgress4Value> getBpfEgress4Map() {
             try {
-                return new BpfMap<>(CLAT_EGRESS4_MAP_PATH,
+                return SingleWriterBpfMap.getSingleton(CLAT_EGRESS4_MAP_PATH,
                        ClatEgress4Key.class, ClatEgress4Value.class);
             } catch (ErrnoException e) {
                 Log.e(TAG, "Cannot create egress4 map: " + e);
@@ -280,6 +281,7 @@
         @Nullable
         public IBpfMap<CookieTagMapKey, CookieTagMapValue> getBpfCookieTagMap() {
             try {
+                // also read and written from other locations
                 return new BpfMap<>(COOKIE_TAG_MAP_PATH,
                        CookieTagMapKey.class, CookieTagMapValue.class);
             } catch (ErrnoException e) {
@@ -683,13 +685,25 @@
             throw new IOException("Failed to start clat ", e);
         } finally {
             if (tunFd != null) {
-                tunFd.close();
+                try {
+                    tunFd.close();
+                } catch (IOException e) {
+                    Log.e(TAG, "Fail to close tun file descriptor " + e);
+                }
             }
             if (readSock6 != null) {
-                readSock6.close();
+                try {
+                    readSock6.close();
+                } catch (IOException e) {
+                    Log.e(TAG, "Fail to close read socket " + e);
+                }
             }
             if (writeSock6 != null) {
-                writeSock6.close();
+                try {
+                    writeSock6.close();
+                } catch (IOException e) {
+                    Log.e(TAG, "Fail to close write socket " + e);
+                }
             }
         }
     }
diff --git a/service/src/com/android/server/connectivity/ConnectivityFlags.java b/service/src/com/android/server/connectivity/ConnectivityFlags.java
index 7ea7f95..1ee1ed7 100644
--- a/service/src/com/android/server/connectivity/ConnectivityFlags.java
+++ b/service/src/com/android/server/connectivity/ConnectivityFlags.java
@@ -46,6 +46,9 @@
 
     public static final String DELAY_DESTROY_SOCKETS = "delay_destroy_sockets";
 
+    public static final String USE_DECLARED_METHODS_FOR_CALLBACKS =
+            "use_declared_methods_for_callbacks";
+
     private boolean mNoRematchAllRequestsOnRegister;
 
     /**
diff --git a/service/src/com/android/server/connectivity/NetworkNotificationManager.java b/service/src/com/android/server/connectivity/NetworkNotificationManager.java
index 7707122..fd41ee6 100644
--- a/service/src/com/android/server/connectivity/NetworkNotificationManager.java
+++ b/service/src/com/android/server/connectivity/NetworkNotificationManager.java
@@ -170,9 +170,11 @@
                     && !TextUtils.isEmpty(nai.linkProperties.getCaptivePortalData()
                     .getVenueFriendlyName())) {
                 name = nai.linkProperties.getCaptivePortalData().getVenueFriendlyName();
+            } else if (!TextUtils.isEmpty(extraInfo)) {
+                name = extraInfo;
             } else {
-                name = TextUtils.isEmpty(extraInfo)
-                        ? WifiInfo.sanitizeSsid(nai.networkCapabilities.getSsid()) : extraInfo;
+                final String ssid = WifiInfo.sanitizeSsid(nai.networkCapabilities.getSsid());
+                name = ssid == null ? "" : ssid;
             }
             // Only notify for Internet-capable networks.
             if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_INTERNET)) return;
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index ede6d3f..e2834b0 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -135,6 +135,7 @@
         "device/com/android/net/module/util/BpfUtils.java",
         "device/com/android/net/module/util/IBpfMap.java",
         "device/com/android/net/module/util/JniUtil.java",
+        "device/com/android/net/module/util/SingleWriterBpfMap.java",
         "device/com/android/net/module/util/TcUtils.java",
     ],
     sdk_version: "module_current",
diff --git a/staticlibs/device/com/android/net/module/util/BpfMap.java b/staticlibs/device/com/android/net/module/util/BpfMap.java
index da77ae8..0fbc25d 100644
--- a/staticlibs/device/com/android/net/module/util/BpfMap.java
+++ b/staticlibs/device/com/android/net/module/util/BpfMap.java
@@ -15,6 +15,7 @@
  */
 package com.android.net.module.util;
 
+import static android.system.OsConstants.EBUSY;
 import static android.system.OsConstants.EEXIST;
 import static android.system.OsConstants.ENOENT;
 
@@ -52,6 +53,9 @@
     public static final int BPF_F_RDONLY = 1 << 3;
     public static final int BPF_F_WRONLY = 1 << 4;
 
+    // magic value for jni consumption, invalid from kernel point of view
+    public static final int BPF_F_RDWR_EXCLUSIVE = BPF_F_RDONLY | BPF_F_WRONLY;
+
     public static final int BPF_MAP_TYPE_HASH = 1;
 
     private static final int BPF_F_NO_PREALLOC = 1;
@@ -69,6 +73,12 @@
     private static ConcurrentHashMap<Pair<String, Integer>, ParcelFileDescriptor> sFdCache =
             new ConcurrentHashMap<>();
 
+    private static ParcelFileDescriptor checkModeExclusivity(ParcelFileDescriptor fd, int mode)
+            throws ErrnoException {
+        if (mode == BPF_F_RDWR_EXCLUSIVE) throw new ErrnoException("cachedBpfFdGet", EBUSY);
+        return fd;
+    }
+
     private static ParcelFileDescriptor cachedBpfFdGet(String path, int mode,
                                                        int keySize, int valueSize)
             throws ErrnoException, NullPointerException {
@@ -79,12 +89,12 @@
         var key = Pair.create(path, (mode << 26) ^ (keySize << 16) ^ valueSize);
         // unlocked fetch is safe: map is concurrent read capable, and only inserted into
         ParcelFileDescriptor fd = sFdCache.get(key);
-        if (fd != null) return fd;
+        if (fd != null) return checkModeExclusivity(fd, mode);
         // ok, no cached fd present, need to grab a lock
         synchronized (BpfMap.class) {
             // need to redo the check
             fd = sFdCache.get(key);
-            if (fd != null) return fd;
+            if (fd != null) return checkModeExclusivity(fd, mode);
             // okay, we really haven't opened this before...
             fd = ParcelFileDescriptor.adoptFd(nativeBpfFdGet(path, mode, keySize, valueSize));
             sFdCache.put(key, fd);
diff --git a/staticlibs/device/com/android/net/module/util/BpfUtils.java b/staticlibs/device/com/android/net/module/util/BpfUtils.java
index cdd6fd7..a41eeba 100644
--- a/staticlibs/device/com/android/net/module/util/BpfUtils.java
+++ b/staticlibs/device/com/android/net/module/util/BpfUtils.java
@@ -39,6 +39,15 @@
     public static final int BPF_CGROUP_INET_SOCK_CREATE = 2;
     public static final int BPF_CGROUP_INET4_BIND = 8;
     public static final int BPF_CGROUP_INET6_BIND = 9;
+    public static final int BPF_CGROUP_INET4_CONNECT = 10;
+    public static final int BPF_CGROUP_INET6_CONNECT = 11;
+    public static final int BPF_CGROUP_UDP4_SENDMSG = 14;
+    public static final int BPF_CGROUP_UDP6_SENDMSG = 15;
+    public static final int BPF_CGROUP_UDP4_RECVMSG = 19;
+    public static final int BPF_CGROUP_UDP6_RECVMSG = 20;
+    public static final int BPF_CGROUP_GETSOCKOPT = 21;
+    public static final int BPF_CGROUP_SETSOCKOPT = 22;
+    public static final int BPF_CGROUP_INET_SOCK_RELEASE = 34;
 
     // Note: This is only guaranteed to be accurate on U+ devices. It is likely to be accurate
     // on T+ devices as well, but this is not guaranteed.
diff --git a/staticlibs/device/com/android/net/module/util/SingleWriterBpfMap.java b/staticlibs/device/com/android/net/module/util/SingleWriterBpfMap.java
new file mode 100644
index 0000000..cd6bfec
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/SingleWriterBpfMap.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.net.module.util;
+
+import android.os.Build;
+import android.system.ErrnoException;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import java.util.HashMap;
+import java.util.NoSuchElementException;
+
+/**
+ * Subclass of BpfMap for maps that are only ever written by one userspace writer.
+ *
+ * This class stores all map data in a userspace HashMap in addition to in the BPF map. This makes
+ * reads (but not iterations) much faster because they do not require a system call or converting
+ * the raw map read to the Value struct. See, e.g., b/343166906 .
+ *
+ * Users of this class must ensure that no BPF program ever writes to the map, and that all
+ * userspace writes to the map occur through this object. Other userspace code may still read from
+ * the map; only writes are required to go through this object.
+ *
+ * Reads and writes to this object are thread-safe and internally synchronized. The read and write
+ * methods are synchronized to ensure that current writers always result in a consistent internal
+ * state (without synchronization, two concurrent writes might update the underlying map and the
+ * cache in the opposite order, resulting in the cache being out of sync with the map).
+ *
+ * getNextKey and iteration over the map are not synchronized or cached and always access the
+ * isunderlying map. The values returned by these calls may be temporarily out of sync with the
+ * values read and written through this object.
+ *
+ * TODO: consider caching reads on iterations as well. This is not trivial because the semantics for
+ * iterating BPF maps require passing in the previously-returned key, and Java iterators only
+ * support iterating from the beginning. It could be done by implementing forEach and possibly by
+ * making getFirstKey and getNextKey private (if no callers are using them). Because HashMap is not
+ * thread-safe, implementing forEach would require either making that method synchronized (and
+ * block reads and updates from other threads until iteration is complete) or switching the
+ * internal HashMap to ConcurrentHashMap.
+ *
+ * @param <K> the key of the map.
+ * @param <V> the value of the map.
+ */
+@RequiresApi(Build.VERSION_CODES.S)
+public class SingleWriterBpfMap<K extends Struct, V extends Struct> extends BpfMap<K, V> {
+    // HashMap instead of ArrayMap because it performs better on larger maps, and many maps used in
+    // our code can contain hundreds of items.
+    private final HashMap<K, V> mCache = new HashMap<>();
+
+    // This should only ever be called (hence private) once for a given 'path'.
+    // Java-wise what matters is the entire {path, key, value} triplet,
+    // but of course the kernel exclusive lock is just on the path (fd),
+    // and any BpfMap has (or should have...) well defined key/value types
+    // (or at least their sizes) so in practice it doesn't really matter.
+    private SingleWriterBpfMap(@NonNull final String path, final Class<K> key,
+            final Class<V> value) throws ErrnoException, NullPointerException {
+        super(path, BPF_F_RDWR_EXCLUSIVE, key, value);
+
+        // Populate cache with the current map contents.
+        K currentKey = super.getFirstKey();
+        while (currentKey != null) {
+            mCache.put(currentKey, super.getValue(currentKey));
+            currentKey = super.getNextKey(currentKey);
+        }
+    }
+
+    // This allows reuse of SingleWriterBpfMap objects for the same {path, keyClass, valueClass}.
+    // These are never destroyed, so once created the lock is (effectively) held till process death
+    // (even if fixed, there would still be a write-only fd cache in underlying BpfMap base class).
+    private static final HashMap<Pair<String, Pair<Class, Class>>, SingleWriterBpfMap>
+            singletonCache = new HashMap<>();
+
+    // This is the public 'factory method' that (creates if needed and) returns a singleton instance
+    // for a given map.  This holds an exclusive lock and has a permanent write-through cache.
+    // It will not be released until process death (or at least unload of the relevant class loader)
+    public synchronized static <KK extends Struct, VV extends Struct> SingleWriterBpfMap<KK,VV>
+            getSingleton(@NonNull final String path, final Class<KK> key, final Class<VV> value)
+            throws ErrnoException, NullPointerException {
+        var cacheKey = new Pair<>(path, new Pair<Class,Class>(key, value));
+        if (!singletonCache.containsKey(cacheKey))
+            singletonCache.put(cacheKey, new SingleWriterBpfMap(path, key, value));
+        return singletonCache.get(cacheKey);
+    }
+
+    @Override
+    public synchronized void updateEntry(K key, V value) throws ErrnoException {
+        super.updateEntry(key, value);
+        mCache.put(key, value);
+    }
+
+    @Override
+    public synchronized void insertEntry(K key, V value)
+            throws ErrnoException, IllegalStateException {
+        super.insertEntry(key, value);
+        mCache.put(key, value);
+    }
+
+    @Override
+    public synchronized void replaceEntry(K key, V value)
+            throws ErrnoException, NoSuchElementException {
+        super.replaceEntry(key, value);
+        mCache.put(key, value);
+    }
+
+    @Override
+    public synchronized boolean insertOrReplaceEntry(K key, V value) throws ErrnoException {
+        final boolean ret = super.insertOrReplaceEntry(key, value);
+        mCache.put(key, value);
+        return ret;
+    }
+
+    @Override
+    public synchronized boolean deleteEntry(K key) throws ErrnoException {
+        final boolean ret = super.deleteEntry(key);
+        mCache.remove(key);
+        return ret;
+    }
+
+    @Override
+    public synchronized boolean containsKey(@NonNull K key) throws ErrnoException {
+        return mCache.containsKey(key);
+    }
+
+    @Override
+    public synchronized V getValue(@NonNull K key) throws ErrnoException {
+        return mCache.get(key);
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
index 19d8bbe..319d51a 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
@@ -180,6 +180,7 @@
     public static final int ICMPV6_NS_HEADER_LEN = 24;
     public static final int ICMPV6_NA_HEADER_LEN = 24;
     public static final int ICMPV6_ND_OPTION_TLLA_LEN = 8;
+    public static final int ICMPV6_ND_OPTION_SLLA_LEN = 8;
 
     public static final int NEIGHBOR_ADVERTISEMENT_FLAG_ROUTER    = 1 << 31;
     public static final int NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED = 1 << 30;
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h b/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h
index d716358..cd51004 100644
--- a/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h
@@ -151,7 +151,7 @@
 
 
 inline base::Result<void> BpfRingbufBase::Init(const char* path) {
-  mRingFd.reset(mapRetrieveRW(path));
+  mRingFd.reset(mapRetrieveExclusiveRW(path));
   if (!mRingFd.ok()) {
     return android::base::ErrnoError()
            << "failed to retrieve ringbuffer at " << path;
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
index dc7925e..e11a9e2 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
@@ -39,11 +39,12 @@
 // Android U / 14 (api level 34) - various new program types added
 #define BPFLOADER_U_VERSION 38u
 
-// Android V / 15 (api level 35) - platform only
+// Android U QPR2 / 14 (api level 34) - platform only
 // (note: the platform bpfloader in V isn't really versioned at all,
 //  as there is no need as it can only load objects compiled at the
 //  same time as itself and the rest of the platform)
-#define BPFLOADER_PLATFORM_VERSION 41u
+#define BPFLOADER_U_QPR2_VERSION 41u
+#define BPFLOADER_PLATFORM_VERSION BPFLOADER_U_QPR2_VERSION
 
 // Android Mainline - this bpfloader should eventually go back to T (or even S)
 // Note: this value (and the following +1u's) are hardcoded in NetBpfLoad.cpp
@@ -55,8 +56,11 @@
 // Android Mainline BpfLoader when running on Android U
 #define BPFLOADER_MAINLINE_U_VERSION (BPFLOADER_MAINLINE_T_VERSION + 1u)
 
+// Android Mainline BpfLoader when running on Android U QPR3
+#define BPFLOADER_MAINLINE_U_QPR3_VERSION (BPFLOADER_MAINLINE_U_VERSION + 1u)
+
 // Android Mainline BpfLoader when running on Android V
-#define BPFLOADER_MAINLINE_V_VERSION (BPFLOADER_MAINLINE_U_VERSION + 1u)
+#define BPFLOADER_MAINLINE_V_VERSION (BPFLOADER_MAINLINE_U_QPR3_VERSION + 1u)
 
 /* For mainline module use, you can #define BPFLOADER_{MIN/MAX}_VER
  * before #include "bpf_helpers.h" to change which bpfloaders will
@@ -113,6 +117,9 @@
     unsigned _btf_min_bpfloader_ver SECTION("btf_min_bpfloader_ver") = 39u; \
     unsigned _btf_user_min_bpfloader_ver SECTION("btf_user_min_bpfloader_ver") = 0xFFFFFFFFu
 
+#define DISABLE_ON_MAINLINE_BEFORE_U_QPR3() \
+    unsigned _netbpfload_min_ver SECTION("netbpfload_min_ver") = BPFLOADER_MAINLINE_U_QPR3_VERSION;
+
 /* flag the resulting bpf .o file as critical to system functionality,
  * loading all kernel version appropriate programs in it must succeed
  * for bpfloader success
@@ -133,6 +140,7 @@
 #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_10 KVER(5, 10, 0)
 #define KVER_5_15 KVER(5, 15, 0)
 #define KVER_6_1  KVER(6, 1, 0)
 #define KVER_6_6  KVER(6, 6, 0)
diff --git a/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h b/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h
index cb02de8..73cef89 100644
--- a/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h
+++ b/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h
@@ -16,8 +16,11 @@
 
 #pragma once
 
+#include <stdlib.h>
+#include <unistd.h>
 #include <linux/bpf.h>
 #include <linux/unistd.h>
+#include <sys/file.h>
 
 #ifdef BPF_FD_JUST_USE_INT
   #define BPF_FD_TYPE int
@@ -128,20 +131,61 @@
                             });
 }
 
-inline int mapRetrieve(const char* pathname, uint32_t flag) {
-    return bpfFdGet(pathname, flag);
+int bpfGetFdMapId(const BPF_FD_TYPE map_fd);
+
+inline int bpfLock(int fd, short type) {
+    if (fd < 0) return fd;  // pass any errors straight through
+#ifdef BPF_MAP_LOCKLESS_FOR_TEST
+    return fd;
+#endif
+#ifdef BPF_FD_JUST_USE_INT
+    int mapId = bpfGetFdMapId(fd);
+    int saved_errno = errno;
+#else
+    base::unique_fd ufd(fd);
+    int mapId = bpfGetFdMapId(ufd);
+    int saved_errno = errno;
+    (void)ufd.release();
+#endif
+    // 4.14+ required to fetch map id, but we don't want to call isAtLeastKernelVersion
+    if (mapId == -1 && saved_errno == EINVAL) return fd;
+    if (mapId <= 0) abort();  // should not be possible
+
+    // on __LP64__ (aka. 64-bit userspace) 'struct flock64' is the same as 'struct flock'
+    struct flock64 fl = {
+        .l_type = type,        // short: F_{RD,WR,UN}LCK
+        .l_whence = SEEK_SET,  // short: SEEK_{SET,CUR,END}
+        .l_start = mapId,      // off_t: start offset
+        .l_len = 1,            // off_t: number of bytes
+    };
+
+    // see: bionic/libc/bionic/fcntl.cpp: iff !__LP64__ this uses fcntl64
+    int ret = fcntl(fd, F_OFD_SETLK, &fl);
+    if (!ret) return fd;  // success
+    close(fd);
+    return ret;  // most likely -1 with errno == EAGAIN, due to already held lock
+}
+
+inline int mapRetrieveLocklessRW(const char* pathname) {
+    return bpfFdGet(pathname, 0);
+}
+
+inline int mapRetrieveExclusiveRW(const char* pathname) {
+    return bpfLock(mapRetrieveLocklessRW(pathname), F_WRLCK);
 }
 
 inline int mapRetrieveRW(const char* pathname) {
-    return mapRetrieve(pathname, 0);
+    return bpfLock(mapRetrieveLocklessRW(pathname), F_RDLCK);
 }
 
 inline int mapRetrieveRO(const char* pathname) {
-    return mapRetrieve(pathname, BPF_F_RDONLY);
+    return bpfFdGet(pathname, BPF_F_RDONLY);
 }
 
+// WARNING: it's impossible to grab a shared (ie. read) lock on a write-only fd,
+// so we instead choose to grab an exclusive (ie. write) lock.
 inline int mapRetrieveWO(const char* pathname) {
-    return mapRetrieve(pathname, BPF_F_WRONLY);
+    return bpfLock(bpfFdGet(pathname, BPF_F_WRONLY), F_WRLCK);
 }
 
 inline int retrieveProgram(const char* pathname) {
diff --git a/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp b/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
index b92f107..1923ceb 100644
--- a/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
+++ b/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
@@ -35,7 +35,24 @@
         jstring path, jint mode, jint keySize, jint valueSize) {
     ScopedUtfChars pathname(env, path);
 
-    jint fd = bpf::bpfFdGet(pathname.c_str(), static_cast<unsigned>(mode));
+    jint fd = -1;
+    switch (mode) {
+      case 0:
+        fd = bpf::mapRetrieveRW(pathname.c_str());
+        break;
+      case BPF_F_RDONLY:
+        fd = bpf::mapRetrieveRO(pathname.c_str());
+        break;
+      case BPF_F_WRONLY:
+        fd = bpf::mapRetrieveWO(pathname.c_str());
+        break;
+      case BPF_F_RDONLY|BPF_F_WRONLY:
+        fd = bpf::mapRetrieveExclusiveRW(pathname.c_str());
+        break;
+      default:
+        errno = EINVAL;
+        break;
+    }
 
     if (fd < 0) {
         jniThrowErrnoException(env, "nativeBpfFdGet", errno);
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java
index 4c3fde6..b5e3dff 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java
@@ -69,11 +69,11 @@
     }
 
     @Test
-    public void testGetValueAsIntger() {
+    public void testGetValueAsInteger() {
         final StructNlAttr attr1 = new StructNlAttr(IFA_FLAGS, TEST_ADDR_FLAGS);
         final Integer integer1 = attr1.getValueAsInteger();
         final int int1 = attr1.getValueAsInt(0x08 /* default value */);
-        assertEquals(integer1, new Integer(TEST_ADDR_FLAGS));
+        assertEquals(integer1, Integer.valueOf(TEST_ADDR_FLAGS));
         assertEquals(int1, TEST_ADDR_FLAGS);
 
         // Malformed attribute.
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 df6067d..e634f0e 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
@@ -34,17 +34,13 @@
     private val connectUtil by lazy { ConnectUtil(context) }
 
     @Test
-    fun testCheckConnectivity() {
-        checkWifiSetup()
-        checkTelephonySetup()
-    }
-
-    private fun checkWifiSetup() {
+    fun testCheckWifiSetup() {
         if (!pm.hasSystemFeature(FEATURE_WIFI)) return
         connectUtil.ensureWifiValidated()
     }
 
-    private fun checkTelephonySetup() {
+    @Test
+    fun testCheckTelephonySetup() {
         if (!pm.hasSystemFeature(FEATURE_TELEPHONY)) return
         val tm = context.getSystemService(TelephonyManager::class.java)
                 ?: fail("Could not get telephony service")
@@ -52,7 +48,7 @@
         val commonError = "Check the test bench. To run the tests anyway for quick & dirty local " +
                 "testing, you can use atest X -- " +
                 "--test-arg com.android.testutils.ConnectivityTestTargetPreparer" +
-                ":ignore-connectivity-check:true"
+                ":ignore-mobile-data-check:true"
         // Do not use assertEquals: it outputs "expected X, was Y", which looks like a test failure
         if (tm.simState == TelephonyManager.SIM_STATE_ABSENT) {
             fail("The device has no SIM card inserted. $commonError")
diff --git a/staticlibs/testutils/host/com/android/testutils/ConnectivityTestTargetPreparer.kt b/staticlibs/testutils/host/com/android/testutils/ConnectivityTestTargetPreparer.kt
index 600a623..435fdd8 100644
--- a/staticlibs/testutils/host/com/android/testutils/ConnectivityTestTargetPreparer.kt
+++ b/staticlibs/testutils/host/com/android/testutils/ConnectivityTestTargetPreparer.kt
@@ -28,9 +28,11 @@
 private const val CONNECTIVITY_CHECKER_APK = "ConnectivityTestPreparer.apk"
 private const val CONNECTIVITY_PKG_NAME = "com.android.testutils.connectivitypreparer"
 private const val CONNECTIVITY_CHECK_CLASS = "$CONNECTIVITY_PKG_NAME.ConnectivityCheckTest"
+
 // As per the <instrumentation> defined in the checker manifest
 private const val CONNECTIVITY_CHECK_RUNNER_NAME = "androidx.test.runner.AndroidJUnitRunner"
-private const val IGNORE_CONN_CHECK_OPTION = "ignore-connectivity-check"
+private const val IGNORE_WIFI_CHECK = "ignore-wifi-check"
+private const val IGNORE_MOBILE_DATA_CHECK = "ignore-mobile-data-check"
 
 // The default updater package names, which might be updating packages while the CTS
 // are running
@@ -41,14 +43,23 @@
  *
  * For quick and dirty local testing, the connectivity check can be disabled by running tests with
  * "atest -- \
- * --test-arg com.android.testutils.ConnectivityTestTargetPreparer:ignore-connectivity-check:true".
+ * --test-arg com.android.testutils.ConnectivityTestTargetPreparer:ignore-mobile-data-check:true". \
+ * --test-arg com.android.testutils.ConnectivityTestTargetPreparer:ignore-wifi-check:true".
  */
 open class ConnectivityTestTargetPreparer : BaseTargetPreparer() {
     private val installer = SuiteApkInstaller()
 
-    @Option(name = IGNORE_CONN_CHECK_OPTION,
-            description = "Disables the check for mobile data and wifi")
-    private var ignoreConnectivityCheck = false
+    @Option(
+        name = IGNORE_WIFI_CHECK,
+            description = "Disables the check for wifi"
+    )
+    private var ignoreWifiCheck = false
+    @Option(
+        name = IGNORE_MOBILE_DATA_CHECK,
+            description = "Disables the check for mobile data"
+    )
+    private var ignoreMobileDataCheck = false
+
     // The default value is never used, but false is a reasonable default
     private var originalTestChainEnabled = false
     private val originalUpdaterPkgsStatus = HashMap<String, Boolean>()
@@ -58,43 +69,62 @@
         disableGmsUpdate(testInfo)
         originalTestChainEnabled = getTestChainEnabled(testInfo)
         originalUpdaterPkgsStatus.putAll(getUpdaterPkgsStatus(testInfo))
-        setUpdaterNetworkingEnabled(testInfo, enableChain = true,
-                enablePkgs = UPDATER_PKGS.associateWith { false })
-        runPreparerApk(testInfo)
+        setUpdaterNetworkingEnabled(
+            testInfo,
+            enableChain = true,
+            enablePkgs = UPDATER_PKGS.associateWith { false }
+        )
+        runConnectivityCheckApk(testInfo)
+        refreshTime(testInfo)
     }
 
-    private fun runPreparerApk(testInfo: TestInformation) {
+    private fun runConnectivityCheckApk(testInfo: TestInformation) {
         installer.setCleanApk(true)
         installer.addTestFileName(CONNECTIVITY_CHECKER_APK)
         installer.setShouldGrantPermission(true)
         installer.setUp(testInfo)
 
+        val testMethods = mutableListOf<String>()
+        if (!ignoreWifiCheck) {
+            testMethods.add("testCheckWifiSetup")
+        }
+        if (!ignoreMobileDataCheck) {
+            testMethods.add("testCheckTelephonySetup")
+        }
+
+        testMethods.forEach {
+            runTestMethod(testInfo, it)
+        }
+    }
+
+    private fun runTestMethod(testInfo: TestInformation, method: String) {
         val runner = DefaultRemoteAndroidTestRunner(
-                CONNECTIVITY_PKG_NAME,
-                CONNECTIVITY_CHECK_RUNNER_NAME,
-                testInfo.device.iDevice)
+            CONNECTIVITY_PKG_NAME,
+            CONNECTIVITY_CHECK_RUNNER_NAME,
+            testInfo.device.iDevice
+        )
         runner.runOptions = "--no-hidden-api-checks"
+        runner.setMethodName(CONNECTIVITY_CHECK_CLASS, method)
 
         val receiver = CollectingTestListener()
         if (!testInfo.device.runInstrumentationTests(runner, receiver)) {
-            throw TargetSetupError("Device state check failed to complete",
-                    testInfo.device.deviceDescriptor)
+            throw TargetSetupError(
+                "Device state check failed to complete",
+                testInfo.device.deviceDescriptor
+            )
         }
 
         val runResult = receiver.currentRunResults
         if (runResult.isRunFailure) {
-            throw TargetSetupError("Failed to check device state before the test: " +
-                    runResult.runFailureMessage, testInfo.device.deviceDescriptor)
-        }
-
-        val ignoredTestClasses = mutableSetOf<String>()
-        if (ignoreConnectivityCheck) {
-            ignoredTestClasses.add(CONNECTIVITY_CHECK_CLASS)
+            throw TargetSetupError(
+                "Failed to check device state before the test: " +
+                    runResult.runFailureMessage,
+                testInfo.device.deviceDescriptor
+            )
         }
 
         val errorMsg = runResult.testResults.mapNotNull { (testDescription, testResult) ->
-            if (TestResult.TestStatus.FAILURE != testResult.status ||
-                    ignoredTestClasses.contains(testDescription.className)) {
+            if (TestResult.TestStatus.FAILURE != testResult.status) {
                 null
             } else {
                 "$testDescription: ${testResult.stackTrace}"
@@ -102,21 +132,27 @@
         }.joinToString("\n")
         if (errorMsg.isBlank()) return
 
-        throw TargetSetupError("Device setup checks failed. Check the test bench: \n$errorMsg",
-                testInfo.device.deviceDescriptor)
+        throw TargetSetupError(
+            "Device setup checks failed. Check the test bench: \n$errorMsg",
+            testInfo.device.deviceDescriptor
+        )
     }
 
     private fun disableGmsUpdate(testInfo: TestInformation) {
         // This will be a no-op on devices without root (su) or not using gservices, but that's OK.
-        testInfo.exec("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")
+                "-e finsky.play_services_auto_update_enabled false"
+        )
     }
 
     private fun clearGmsUpdateOverride(testInfo: TestInformation) {
-        testInfo.exec("su 0 am broadcast " +
+        testInfo.exec(
+            "su 0 am broadcast " +
                 "-a com.google.gservices.intent.action.GSERVICES_OVERRIDE " +
-                "--esn finsky.play_services_auto_update_enabled")
+                "--esn finsky.play_services_auto_update_enabled"
+        )
     }
 
     private fun setUpdaterNetworkingEnabled(
@@ -136,17 +172,27 @@
             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")
-            }
+        UPDATER_PKGS.associateWith { pkg ->
+            !testInfo.exec("cmd connectivity get-package-networking-enabled $pkg")
+                .contains(":deny")
+        }
+
+    private fun refreshTime(testInfo: TestInformation,) {
+        // Forces a synchronous time refresh using the network. Time is fetched synchronously but
+        // this does not guarantee that system time is updated when it returns.
+        // This avoids flakes where the system clock rolls back, for example when using test
+        // settings like test_url_expiration_time in NetworkMonitor.
+        testInfo.exec("cmd network_time_update_service force_refresh")
+    }
 
     override fun tearDown(testInfo: TestInformation, e: Throwable?) {
         if (isTearDownDisabled) return
         installer.tearDown(testInfo, e)
-        setUpdaterNetworkingEnabled(testInfo,
-                enableChain = originalTestChainEnabled,
-                enablePkgs = originalUpdaterPkgsStatus)
+        setUpdaterNetworkingEnabled(
+            testInfo,
+            enableChain = originalTestChainEnabled,
+            enablePkgs = originalUpdaterPkgsStatus
+        )
         clearGmsUpdateOverride(testInfo)
     }
 }
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index 6eb56c7b..0f0e2f1 100644
--- a/tests/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -61,6 +61,7 @@
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
 import static android.os.Process.INVALID_UID;
+
 import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
 import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
 import static com.android.modules.utils.build.SdkLevel.isAtLeastV;
@@ -68,6 +69,7 @@
 import static com.android.testutils.MiscAsserts.assertEmpty;
 import static com.android.testutils.MiscAsserts.assertThrows;
 import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -83,21 +85,25 @@
 import android.os.Build;
 import android.util.ArraySet;
 import android.util.Range;
+
 import androidx.test.filters.SmallTest;
+
 import com.android.testutils.CompatUtil;
 import com.android.testutils.ConnectivityModuleTest;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRunner;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Set;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mockito;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
 @SmallTest
 @RunWith(DevSdkIgnoreRunner.class)
 // NetworkCapabilities is only updatable on S+, and this test covers behavior which implementation
@@ -1465,4 +1471,26 @@
         assertEquals("-SUPL-VALIDATED-CAPTIVE_PORTAL+MMS+OEM_PAID",
                 nc1.describeCapsDifferencesFrom(nc2));
     }
+
+    @Test
+    public void testInvalidCapability() {
+        final int invalidCapability = Integer.MAX_VALUE;
+        // Passing invalid capability does not throw
+        final NetworkCapabilities nc1 = new NetworkCapabilities.Builder()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addForbiddenCapability(NET_CAPABILITY_NOT_ROAMING)
+                .removeCapability(invalidCapability)
+                .removeForbiddenCapability(invalidCapability)
+                .addCapability(invalidCapability)
+                .addForbiddenCapability(invalidCapability)
+                .build();
+
+        final NetworkCapabilities nc2 = new NetworkCapabilities.Builder()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addForbiddenCapability(NET_CAPABILITY_NOT_ROAMING)
+                .build();
+
+        // nc1 and nc2 are the same since invalid capability is ignored
+        assertEquals(nc1, nc2);
+    }
 }
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractDefaultRestrictionsTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractDefaultRestrictionsTest.java
index da633c0..00f67f4 100644
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractDefaultRestrictionsTest.java
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractDefaultRestrictionsTest.java
@@ -16,7 +16,7 @@
 
 package com.android.cts.netpolicy.hostside;
 
-import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING;
+import static android.app.ActivityManager.PROCESS_STATE_LAST_ACTIVITY;
 
 import static org.junit.Assume.assumeTrue;
 
@@ -46,14 +46,15 @@
     public final void tearDown() throws Exception {
         super.tearDown();
 
+        stopApp();
         removePowerSaveModeWhitelist(TEST_APP2_PKG);
         removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
     }
 
     @Test
     public void testFgsNetworkAccess() throws Exception {
-        assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
-        SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+        assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
+        SystemClock.sleep(mProcessStateTransitionShortDelayMs);
         assertNetworkAccess(false, null);
 
         launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
@@ -61,8 +62,8 @@
 
     @Test
     public void testActivityNetworkAccess() throws Exception {
-        assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
-        SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+        assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
+        SystemClock.sleep(mProcessStateTransitionShortDelayMs);
         assertNetworkAccess(false, null);
 
         launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
@@ -70,23 +71,23 @@
 
     @Test
     public void testBackgroundNetworkAccess_inFullAllowlist() throws Exception {
-        assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
-        SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+        assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
+        SystemClock.sleep(mProcessStateTransitionShortDelayMs);
         assertNetworkAccess(false, null);
 
         addPowerSaveModeWhitelist(TEST_APP2_PKG);
-        assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+        assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
         assertNetworkAccess(true, null);
     }
 
     @Test
     public void testBackgroundNetworkAccess_inExceptIdleAllowlist() throws Exception {
-        assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
-        SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+        assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
+        SystemClock.sleep(mProcessStateTransitionShortDelayMs);
         assertNetworkAccess(false, null);
 
         addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-        assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+        assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
         assertNetworkAccess(true, null);
     }
 }
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractRestrictBackgroundNetworkTestCase.java
index d0203c5..0f5f58c 100644
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractRestrictBackgroundNetworkTestCase.java
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractRestrictBackgroundNetworkTestCase.java
@@ -96,7 +96,12 @@
     protected static final String TEST_PKG = "com.android.cts.netpolicy.hostside";
     protected static final String TEST_APP2_PKG = "com.android.cts.netpolicy.hostside.app2";
     // TODO(b/321797685): Configure it via device-config once it is available.
-    protected static final long PROCESS_STATE_TRANSITION_DELAY_MS = TimeUnit.SECONDS.toMillis(5);
+    protected final long mProcessStateTransitionLongDelayMs =
+            useDifferentDelaysForBackgroundChain() ? TimeUnit.SECONDS.toMillis(20)
+                    : TimeUnit.SECONDS.toMillis(5);
+    protected final long mProcessStateTransitionShortDelayMs =
+            useDifferentDelaysForBackgroundChain() ? TimeUnit.SECONDS.toMillis(2)
+                    : TimeUnit.SECONDS.toMillis(5);
 
     private static final String TEST_APP2_ACTIVITY_CLASS = TEST_APP2_PKG + ".MyActivity";
     private static final String TEST_APP2_SERVICE_CLASS = TEST_APP2_PKG + ".MyForegroundService";
@@ -241,6 +246,22 @@
         return Boolean.parseBoolean(output);
     }
 
+    /**
+     * Check if the flag to use different delays for sensitive proc-states is enabled.
+     * This is a manual check because the feature flag infrastructure may not be available
+     * in all the branches that will get this code.
+     * TODO: b/322115994 - Use @RequiresFlagsEnabled with
+     * Flags.FLAG_USE_DIFFERENT_DELAYS_FOR_BACKGROUND_CHAIN once the tests are moved to cts.
+     */
+    private boolean useDifferentDelaysForBackgroundChain() {
+        if (!SdkLevel.isAtLeastV()) {
+            return false;
+        }
+        final String output = executeShellCommand("device_config get backstage_power"
+                + " com.android.server.net.use_different_delays_for_background_chain");
+        return Boolean.parseBoolean(output);
+    }
+
     protected int getUid(String packageName) throws Exception {
         return mContext.getPackageManager().getPackageUid(packageName, 0);
     }
@@ -824,6 +845,10 @@
         assertDelayedShellCommand("dumpsys deviceidle get deep", enabled ? "IDLE" : "ACTIVE");
     }
 
+    protected void stopApp() {
+        executeSilentShellCommand("am stop-app " + TEST_APP2_PKG);
+    }
+
     protected void setAppIdle(boolean isIdle) throws Exception {
         setAppIdleNoAssert(isIdle);
         assertAppIdle(isIdle);
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ConnOnActivityStartTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ConnOnActivityStartTest.java
index 811190f..bfccce9 100644
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ConnOnActivityStartTest.java
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ConnOnActivityStartTest.java
@@ -53,7 +53,7 @@
     @After
     public final void tearDown() throws Exception {
         super.tearDown();
-        finishActivity();
+        stopApp();
         resetDeviceState();
     }
 
@@ -108,7 +108,7 @@
         assumeTrue("Feature not enabled", isNetworkBlockedForTopSleepingAndAbove());
         assertLaunchedActivityHasNetworkAccess("testStartActivity_default", () -> {
             assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
-            SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+            SystemClock.sleep(mProcessStateTransitionLongDelayMs);
             assertNetworkAccess(false, null);
         });
     }
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkCallbackTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkCallbackTest.java
index 7038d02..3934cfa 100644
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkCallbackTest.java
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkCallbackTest.java
@@ -268,6 +268,7 @@
         setRestrictBackground(false);
         setBatterySaverMode(false);
         unregisterNetworkCallback();
+        stopApp();
 
         if (SdkLevel.isAtLeastT() && (mCtsNetUtils != null)) {
             mCtsNetUtils.restorePrivateDnsSetting();
@@ -387,7 +388,7 @@
 
             finishActivity();
             assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
-            SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+            SystemClock.sleep(mProcessStateTransitionLongDelayMs);
             assertNetworkAccess(false, null);
             mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
             assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
@@ -413,7 +414,7 @@
 
             finishActivity();
             assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
-            SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+            SystemClock.sleep(mProcessStateTransitionLongDelayMs);
             assertNetworkAccess(false, null);
             mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
             assertNetworkAccessBlockedByBpf(true, mUid, false /* metered */);
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyManagerTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyManagerTest.java
index 9b3fe9f..6c5f2ff 100644
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyManagerTest.java
+++ b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyManagerTest.java
@@ -17,6 +17,7 @@
 package com.android.cts.netpolicy.hostside;
 
 import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+import static android.app.ActivityManager.PROCESS_STATE_LAST_ACTIVITY;
 import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING;
 import static android.os.Process.SYSTEM_UID;
 
@@ -65,6 +66,7 @@
         setRestrictBackground(false);
         setRestrictedNetworkingMode(false);
         unregisterNetworkCallback();
+        stopApp();
     }
 
     @Test
@@ -248,8 +250,8 @@
         assumeTrue("Feature not enabled", isNetworkBlockedForTopSleepingAndAbove());
 
         try {
-            assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
-            SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+            assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
+            SystemClock.sleep(mProcessStateTransitionShortDelayMs);
             assertNetworkingBlockedStatusForUid(mUid, METERED, true /* expectedResult */);
             assertTrue(isUidNetworkingBlocked(mUid, NON_METERED));
 
@@ -260,7 +262,7 @@
 
             finishActivity();
             assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
-            SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+            SystemClock.sleep(mProcessStateTransitionLongDelayMs);
             assertNetworkingBlockedStatusForUid(mUid, METERED, true /* expectedResult */);
             assertTrue(isUidNetworkingBlocked(mUid, NON_METERED));
 
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
index e88c105..e186c6b 100755
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
@@ -971,8 +971,12 @@
         final TestableNetworkCallback otherUidCallback = new TestableNetworkCallback();
         final TestableNetworkCallback myUidCallback = new TestableNetworkCallback();
         if (SdkLevel.isAtLeastS()) {
-            final int otherUid =
-                    UserHandle.of(5 /* userId */).getUid(Process.FIRST_APPLICATION_UID);
+            // Using the same appId with the test to make sure otherUid has the internet permission.
+            // This works because the UID permission map only stores the app ID and not the whole
+            // UID. If the otherUid does not have the internet permission, network access from
+            // otherUid could be considered blocked on V+.
+            final int appId = UserHandle.getAppId(Process.myUid());
+            final int otherUid = UserHandle.of(5 /* userId */).getUid(appId);
             final Handler h = new Handler(Looper.getMainLooper());
             runWithShellPermissionIdentity(() -> {
                 registerSystemDefaultNetworkCallback(systemDefaultCallback, h);
diff --git a/tests/cts/hostside/app2/Android.bp b/tests/cts/hostside/app2/Android.bp
index ad25562..12ea23b 100644
--- a/tests/cts/hostside/app2/Android.bp
+++ b/tests/cts/hostside/app2/Android.bp
@@ -35,4 +35,5 @@
         "general-tests",
         "sts",
     ],
+    sdk_version: "test_current",
 }
diff --git a/tests/cts/hostside/networkslicingtestapp/Android.bp b/tests/cts/hostside/networkslicingtestapp/Android.bp
index 100b6e4..c220000 100644
--- a/tests/cts/hostside/networkslicingtestapp/Android.bp
+++ b/tests/cts/hostside/networkslicingtestapp/Android.bp
@@ -44,6 +44,7 @@
         "CtsHostsideNetworkCapTestsAppDefaults",
     ],
     manifest: "AndroidManifestWithoutProperty.xml",
+    sdk_version: "test_current",
 }
 
 android_test_helper_app {
@@ -53,6 +54,7 @@
         "CtsHostsideNetworkCapTestsAppDefaults",
     ],
     manifest: "AndroidManifestWithProperty.xml",
+    sdk_version: "test_current",
 }
 
 android_test_helper_app {
@@ -63,4 +65,5 @@
     ],
     target_sdk_version: "33",
     manifest: "AndroidManifestWithoutProperty.xml",
+    sdk_version: "test_current",
 }
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
index f0a87af..cea60f9 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
@@ -18,8 +18,6 @@
 
 import android.platform.test.annotations.RequiresDevice;
 
-import com.android.testutils.SkipPresubmit;
-
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -37,13 +35,11 @@
         uninstallPackage(TEST_APP2_PKG, true);
     }
 
-    @SkipPresubmit(reason = "Out of SLO flakiness")
     @Test
     public void testChangeUnderlyingNetworks() throws Exception {
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testChangeUnderlyingNetworks");
     }
 
-    @SkipPresubmit(reason = "Out of SLO flakiness")
     @Test
     public void testDefault() throws Exception {
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testDefault");
@@ -166,7 +162,6 @@
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testBlockIncomingPackets");
     }
 
-    @SkipPresubmit(reason = "Out of SLO flakiness")
     @Test
     public void testSetVpnDefaultForUids() throws Exception {
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testSetVpnDefaultForUids");
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index 768ba12..ae85701 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -180,4 +180,5 @@
         "cts",
         "general-tests",
     ],
+    sdk_version: "test_current",
 }
diff --git a/tests/cts/net/AndroidTestTemplate.xml b/tests/cts/net/AndroidTestTemplate.xml
index 38f26d8..077c3ef 100644
--- a/tests/cts/net/AndroidTestTemplate.xml
+++ b/tests/cts/net/AndroidTestTemplate.xml
@@ -24,6 +24,11 @@
     <option name="config-descriptor:metadata" key="mainline-param" value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk" />
     <option name="config-descriptor:metadata" key="mainline-param" value="com.google.android.tethering.apex" />
     <option name="not-shardable" value="true" />
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.DynamicConfigPusher">
+        <option name="target" value="device" />
+        <option name="config-filename" value="{MODULE}" />
+        <option name="version" value="1.0" />
+    </target_preparer>
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true" />
         <option name="test-file-name" value="{MODULE}.apk" />
@@ -38,6 +43,7 @@
         <option name="runtime-hint" value="9m4s" />
         <option name="hidden-api-checks" value="false" />
         <option name="isolated-storage" value="false" />
+        <option name="instrumentation-arg" key="test-module-name" value="{MODULE}" />
         <!-- Test filter that allows test APKs to select which tests they want to run by annotating
              those tests with an annotation matching the name of the APK.
 
diff --git a/tests/cts/net/DynamicConfig.xml b/tests/cts/net/DynamicConfig.xml
new file mode 100644
index 0000000..af019c2
--- /dev/null
+++ b/tests/cts/net/DynamicConfig.xml
@@ -0,0 +1,24 @@
+<!--
+  ~ Copyright (C) 2024 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.
+  -->
+
+<dynamicConfig>
+    <entry key="remote_config_required">
+        <value>false</value>
+    </entry>
+    <entry key="IP_ADDRESS_ECHO_URL">
+        <value>https://google-ipv6test.appspot.com/ip.js?fmt=text</value>
+    </entry>
+</dynamicConfig>
diff --git a/tests/cts/net/api23Test/Android.bp b/tests/cts/net/api23Test/Android.bp
index 2ec3a70..587d5a5 100644
--- a/tests/cts/net/api23Test/Android.bp
+++ b/tests/cts/net/api23Test/Android.bp
@@ -56,4 +56,5 @@
         ":CtsNetTestAppForApi23",
     ],
     per_testcase_directory: true,
+    sdk_version: "test_current",
 }
diff --git a/tests/cts/net/native/dns/Android.bp b/tests/cts/net/native/dns/Android.bp
index 8e24fba..de4a3bf 100644
--- a/tests/cts/net/native/dns/Android.bp
+++ b/tests/cts/net/native/dns/Android.bp
@@ -3,6 +3,11 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+filegroup {
+    name: "dns_async_test_default_map",
+    srcs: ["dns_async_test_default.map"],
+}
+
 cc_defaults {
     name: "dns_async_defaults",
 
@@ -20,6 +25,14 @@
     srcs: [
         "NativeDnsAsyncTest.cpp",
     ],
+    // This test runs on older platform versions, so many libraries (such as libbase and libc++)
+    // need to be linked statically. The test also needs to be linked with a version script to
+    // ensure that the statically-linked library isn't exported from the executable, where it
+    // would override the shared libraries that the platform itself uses.
+    // See http://b/333438055 for an example of what goes wrong when libc++ is partially exported
+    // from an executable.
+    version_script: ":dns_async_test_default_map",
+    stl: "libc++_static",
     shared_libs: [
         "libandroid",
         "liblog",
diff --git a/tests/cts/net/native/dns/dns_async_test_default.map b/tests/cts/net/native/dns/dns_async_test_default.map
new file mode 100644
index 0000000..e342e43
--- /dev/null
+++ b/tests/cts/net/native/dns/dns_async_test_default.map
@@ -0,0 +1,20 @@
+#
+# Copyright (C) 2024 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.
+#
+
+{
+  local:
+    *;
+};
diff --git a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
index 90fb7a9..d233fee 100644
--- a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
+++ b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
@@ -55,6 +55,7 @@
 import com.android.compatibility.common.util.PropertyUtil.getVsrApiLevel
 import com.android.compatibility.common.util.SystemUtil.runShellCommand
 import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
+import com.android.compatibility.common.util.VsrTest
 import com.android.internal.util.HexDump
 import com.android.net.module.util.PacketReader
 import com.android.testutils.DevSdkIgnoreRule
@@ -300,6 +301,10 @@
         }
     }
 
+    @VsrTest(
+        requirements = ["VSR-5.3.12-001", "VSR-5.3.12-003", "VSR-5.3.12-004", "VSR-5.3.12-009",
+            "VSR-5.3.12-012"]
+    )
     @Test
     fun testApfCapabilities() {
         // APF became mandatory in Android 14 VSR.
@@ -350,10 +355,14 @@
         return HexDump.hexStringToByteArray(progHexString)
     }
 
+    @VsrTest(
+            requirements = ["VSR-5.3.12-007", "VSR-5.3.12-008", "VSR-5.3.12-010", "VSR-5.3.12-011"]
+    )
     @SkipPresubmit(reason = "This test takes longer than 1 minute, do not run it on presubmit.")
     // APF integration is mostly broken before V, only run the full read / write test on V+.
     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
-    @Test
+    // Increase timeout for test to 15 minutes to accommodate device with large APF RAM.
+    @Test(timeout = 15 * 60 * 1000)
     fun testReadWriteProgram() {
         assumeApfVersionSupportAtLeast(4)
 
@@ -400,6 +409,7 @@
     }
 
     // APF integration is mostly broken before V
+    @VsrTest(requirements = ["VSR-5.3.12-002", "VSR-5.3.12-005"])
     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @Test
     fun testDropPingReply() {
@@ -448,6 +458,7 @@
     fun clearApfMemory() = installProgram(ByteArray(caps.maximumApfProgramSize))
 
     // APF integration is mostly broken before V
+    @VsrTest(requirements = ["VSR-5.3.12-002", "VSR-5.3.12-005"])
     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @Test
     fun testPrefilledMemorySlotsV4() {
@@ -503,6 +514,7 @@
     }
 
     // APF integration is mostly broken before V
+    @VsrTest(requirements = ["VSR-5.3.12-002", "VSR-5.3.12-005"])
     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @Test
     fun testFilterAgeIncreasesBetweenPackets() {
@@ -542,6 +554,7 @@
         buffer = ByteBuffer.wrap(readProgram(), counterRegion, 4 /* length */)
         val filterAgeSeconds = buffer.getInt()
         // Assert that filter age has increased, but not too much.
-        assertThat(filterAgeSeconds - filterAgeSecondsOrig).isEqualTo(5)
+        val timeDiff = filterAgeSeconds - filterAgeSecondsOrig
+        assertThat(timeDiff).isAnyOf(5, 6)
     }
 }
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index bf40462..21eb90f 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -174,6 +174,7 @@
 import android.net.wifi.WifiManager;
 import android.os.Binder;
 import android.os.Build;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.MessageQueue;
@@ -192,10 +193,11 @@
 import android.util.Log;
 import android.util.Range;
 
-import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.RequiresDevice;
+import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.compatibility.common.util.DynamicConfigDeviceSide;
 import com.android.internal.util.ArrayUtils;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.CollectionUtils;
@@ -213,7 +215,6 @@
 import com.android.testutils.DeviceInfoUtils;
 import com.android.testutils.DumpTestUtils;
 import com.android.testutils.RecorderCallback.CallbackEntry;
-import com.android.testutils.SkipMainlinePresubmit;
 import com.android.testutils.SkipPresubmit;
 import com.android.testutils.TestHttpServer;
 import com.android.testutils.TestNetworkTracker;
@@ -249,6 +250,7 @@
 import java.net.UnknownHostException;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
@@ -309,6 +311,7 @@
 
     // Airplane Mode BroadcastReceiver Timeout
     private static final long AIRPLANE_MODE_CHANGE_TIMEOUT_MS = 10_000L;
+    private static final long CELL_DATA_AVAILABLE_TIMEOUT_MS = 120_000L;
 
     // Timeout for applying uids allowed on restricted networks
     private static final long APPLYING_UIDS_ALLOWED_ON_RESTRICTED_NETWORKS_TIMEOUT_MS = 3_000L;
@@ -334,6 +337,11 @@
     private static final String TEST_HTTPS_URL_PATH = "/https_path";
     private static final String TEST_HTTP_URL_PATH = "/http_path";
     private static final String LOCALHOST_HOSTNAME = "localhost";
+    private static final String TEST_MODULE_NAME_OPTION = "test-module-name";
+    private static final String IP_ADDRESS_ECHO_URL_KEY = "IP_ADDRESS_ECHO_URL";
+    private static final List<String> ALLOWED_IP_ADDRESS_ECHO_URLS = Arrays.asList(
+            "https://google-ipv6test.appspot.com/ip.js?fmt=text",
+            "https://ipv6test.googleapis-cn.com/ip.js?fmt=text");
     // Re-connecting to the AP, obtaining an IP address, revalidating can take a long time
     private static final long WIFI_CONNECT_TIMEOUT_MS = 60_000L;
 
@@ -854,7 +862,7 @@
      * Tests that connections can be opened on WiFi and cellphone networks,
      * and that they are made from different IP addresses.
      */
-    @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+    @AppModeFull(reason = "Cannot get WifiManager or access the SD card in instant app mode")
     @Test
     @RequiresDevice // Virtual devices use a single internet connection for all networks
     public void testOpenConnection() throws Exception {
@@ -864,7 +872,8 @@
         Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
         Network cellNetwork = networkCallbackRule.requestCell();
         // This server returns the requestor's IP address as the response body.
-        URL url = new URL("http://google-ipv6test.appspot.com/ip.js?fmt=text");
+        String ipAddressEchoUrl = getIpAddressEchoUrlFromConfig();
+        URL url = new URL(ipAddressEchoUrl);
         String wifiAddressString = httpGet(wifiNetwork, url);
         String cellAddressString = httpGet(cellNetwork, url);
 
@@ -881,6 +890,19 @@
     }
 
     /**
+     * Gets IP address echo url from dynamic config.
+     */
+    private static String getIpAddressEchoUrlFromConfig() throws Exception {
+        Bundle instrumentationArgs = InstrumentationRegistry.getArguments();
+        String testModuleName = instrumentationArgs.getString(TEST_MODULE_NAME_OPTION);
+        // Get the DynamicConfig.xml contents and extract the ipv6 test URL.
+        DynamicConfigDeviceSide dynamicConfig = new DynamicConfigDeviceSide(testModuleName);
+        String ipAddressEchoUrl = dynamicConfig.getValue(IP_ADDRESS_ECHO_URL_KEY);
+        assertContains(ALLOWED_IP_ADDRESS_ECHO_URLS, ipAddressEchoUrl);
+        return ipAddressEchoUrl;
+    }
+
+    /**
      * Performs a HTTP GET to the specified URL on the specified Network, and returns
      * the response body decoded as UTF-8.
      */
@@ -1031,7 +1053,6 @@
 
     @AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps")
     @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
-    @SkipMainlinePresubmit(reason = "Out of SLO flakiness")
     public void testIsPrivateDnsBroken() throws InterruptedException {
         final String invalidPrivateDnsServer = "invalidhostname.example.com";
         final String goodPrivateDnsServer = "dns.google";
@@ -1236,13 +1257,14 @@
             final IntentFilter filter = new IntentFilter();
             filter.addAction(broadcastAction);
 
+            final CompletableFuture<NetworkRequest> requestFuture = new CompletableFuture<>();
             final CompletableFuture<Network> networkFuture = new CompletableFuture<>();
             final AtomicInteger receivedCount = new AtomicInteger(0);
             receiver = new BroadcastReceiver() {
                 @Override
                 public void onReceive(Context context, Intent intent) {
                     final NetworkRequest request = intent.getParcelableExtra(EXTRA_NETWORK_REQUEST);
-                    assertPendingIntentRequestMatches(request, secondRequest, useListen);
+                    requestFuture.complete(request);
                     receivedCount.incrementAndGet();
                     networkFuture.complete(intent.getParcelableExtra(EXTRA_NETWORK));
                 }
@@ -1257,6 +1279,9 @@
             } catch (TimeoutException e) {
                 throw new AssertionError("PendingIntent not received for " + secondRequest, e);
             }
+            assertPendingIntentRequestMatches(
+                    requestFuture.get(NETWORK_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS),
+                    secondRequest, useListen);
 
             // Sleep for a small amount of time to try to check that only one callback is ever
             // received (so the first callback was really unregistered). This does not guarantee
@@ -2242,7 +2267,10 @@
             // connectToCell only registers a request, it cannot / does not need to be called twice
             mCtsNetUtils.ensureWifiConnected();
             if (verifyWifi) waitForAvailable(wifiCb);
-            if (supportTelephony) waitForAvailable(telephonyCb);
+            if (supportTelephony) {
+                telephonyCb.eventuallyExpect(
+                        CallbackEntry.AVAILABLE, CELL_DATA_AVAILABLE_TIMEOUT_MS);
+            }
         } finally {
             // Restore the previous state of airplane mode and permissions:
             runShellCommand("cmd connectivity airplane-mode "
diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
index 6fa2812..61ebd8f 100644
--- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
@@ -303,18 +303,6 @@
         fun expectOnAvailable(timeout: Long = TIMEOUT_MS): String {
             return available.get(timeout, TimeUnit.MILLISECONDS)
         }
-
-        fun expectOnUnavailable() {
-            // Assert that the future fails with the IllegalStateException from the
-            // completeExceptionally() call inside onUnavailable.
-            assertFailsWith(IllegalStateException::class) {
-                try {
-                    available.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
-                } catch (e: ExecutionException) {
-                    throw e.cause!!
-                }
-            }
-        }
     }
 
     private class EthernetOutcomeReceiver :
diff --git a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
index 5b53839..ff10e1a 100644
--- a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
@@ -19,19 +19,19 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_FOTA;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_SUPL;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
 import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
 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.testutils.DevSdkIgnoreRuleKt.VANILLA_ICE_CREAM;
-
 import static com.google.common.truth.Truth.assertThat;
 
 import static junit.framework.Assert.fail;
@@ -66,9 +66,9 @@
 import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
 import com.android.testutils.ConnectivityModuleTest;
 import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 
-import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -512,30 +512,20 @@
         assertArrayEquals(netCapabilities, nr.getCapabilities());
     }
 
-    @Test @IgnoreUpTo(VANILLA_ICE_CREAM) @Ignore("b/338200742")
+    // Default capabilities and default forbidden capabilities must not be changed on U- because
+    // this could cause the system server crash when there is a module rollback (b/313030307)
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R) @IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void testDefaultCapabilities() {
         final NetworkRequest defaultNR = new NetworkRequest.Builder().build();
-        assertTrue(defaultNR.hasForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK));
-        assertFalse(defaultNR.hasCapability(NET_CAPABILITY_LOCAL_NETWORK));
+
+        assertEquals(4, defaultNR.getCapabilities().length);
+        assertTrue(defaultNR.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+        assertTrue(defaultNR.hasCapability(NET_CAPABILITY_TRUSTED));
         assertTrue(defaultNR.hasCapability(NET_CAPABILITY_NOT_VPN));
+        assertTrue(defaultNR.hasCapability(NET_CAPABILITY_NOT_VCN_MANAGED));
 
-        final NetworkCapabilities emptyNC =
-                NetworkCapabilities.Builder.withoutDefaultCapabilities().build();
-        assertFalse(defaultNR.canBeSatisfiedBy(emptyNC));
-
-        // defaultNC represent the capabilities of a network agent, so they must not contain
-        // forbidden capabilities by default.
-        final NetworkCapabilities defaultNC = new NetworkCapabilities.Builder().build();
-        assertArrayEquals(new int[0], defaultNC.getForbiddenCapabilities());
-        // A default NR can be satisfied by default NC.
-        assertTrue(defaultNR.canBeSatisfiedBy(defaultNC));
-
-        // Conversely, network requests have forbidden capabilities by default to manage
-        // backward compatibility, so test that these forbidden capabilities are in place.
-        // Starting in V, NET_CAPABILITY_LOCAL_NETWORK is introduced but is not seen by
-        // default, thanks to a default forbidden capability in NetworkRequest.
-        defaultNC.addCapability(NET_CAPABILITY_LOCAL_NETWORK);
-        assertFalse(defaultNR.canBeSatisfiedBy(defaultNC));
+        assertEquals(0, defaultNR.getForbiddenCapabilities().length);
     }
 
     @Test
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
index 1b1f367..a3c3f45 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
@@ -72,7 +72,7 @@
 
         tryTest {
             downstreamIface = createTestInterface()
-            val iface = tetheredInterface
+            val iface = mTetheredInterfaceRequester.getInterface()
             assertEquals(iface, downstreamIface?.interfaceName)
             val request = TetheringRequest.Builder(TETHERING_ETHERNET)
                 .setConnectivityScope(CONNECTIVITY_SCOPE_LOCAL).build()
@@ -115,7 +115,7 @@
 
         tryTest {
             downstreamIface = createTestInterface()
-            val iface = tetheredInterface
+            val iface = mTetheredInterfaceRequester.getInterface()
             assertEquals(iface, downstreamIface?.interfaceName)
 
             val localAddr = LinkAddress("192.0.2.3/28")
diff --git a/tests/cts/netpermission/internetpermission/Android.bp b/tests/cts/netpermission/internetpermission/Android.bp
index 7d5ca2f..e0424ac 100644
--- a/tests/cts/netpermission/internetpermission/Android.bp
+++ b/tests/cts/netpermission/internetpermission/Android.bp
@@ -31,4 +31,5 @@
         "general-tests",
     ],
     host_required: ["net-tests-utils-host-common"],
+    sdk_version: "test_current",
 }
diff --git a/tests/cts/netpermission/updatestatspermission/Android.bp b/tests/cts/netpermission/updatestatspermission/Android.bp
index 2fde1ce..689ce74 100644
--- a/tests/cts/netpermission/updatestatspermission/Android.bp
+++ b/tests/cts/netpermission/updatestatspermission/Android.bp
@@ -19,11 +19,17 @@
 
 android_test {
     name: "CtsNetTestCasesUpdateStatsPermission",
-    defaults: ["cts_defaults"],
+    defaults: [
+        "cts_defaults",
+        "framework-connectivity-test-defaults",
+    ],
 
     srcs: ["src/**/*.java"],
-
-    static_libs: ["ctstestrunner-axt"],
+    platform_apis: true,
+    static_libs: [
+        "ctstestrunner-axt",
+        "net-tests-utils",
+    ],
 
     // Tag this module as a cts test artifact
     test_suites: [
diff --git a/tests/cts/netpermission/updatestatspermission/src/android/net/cts/network/permission/UpdateStatsPermissionTest.java b/tests/cts/netpermission/updatestatspermission/src/android/net/cts/network/permission/UpdateStatsPermissionTest.java
index bea843c..56bad31 100644
--- a/tests/cts/netpermission/updatestatspermission/src/android/net/cts/network/permission/UpdateStatsPermissionTest.java
+++ b/tests/cts/netpermission/updatestatspermission/src/android/net/cts/network/permission/UpdateStatsPermissionTest.java
@@ -16,6 +16,10 @@
 
 package android.net.cts.networkpermission.updatestatspermission;
 
+import static android.Manifest.permission.NETWORK_SETTINGS;
+
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
@@ -25,6 +29,8 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.modules.utils.build.SdkLevel;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -67,6 +73,11 @@
         out.write(buf);
         out.close();
         socket.close();
+        // Clear TrafficStats cache is needed to avoid rate-limit caching for
+        // TrafficStats API results on V+ devices.
+        if (SdkLevel.isAtLeastV()) {
+            runAsShell(NETWORK_SETTINGS, () -> TrafficStats.clearRateLimitCaches());
+        }
         long uidTxBytesAfter = TrafficStats.getUidTxBytes(Process.myUid());
         long uidTxDeltaBytes = uidTxBytesAfter - uidTxBytesBefore;
         assertTrue("uidtxb: " + uidTxBytesBefore + " -> " + uidTxBytesAfter + " delta="
diff --git a/tests/mts/bpf_existence_test.cpp b/tests/mts/bpf_existence_test.cpp
index 51a4eca..b02c61c 100644
--- a/tests/mts/bpf_existence_test.cpp
+++ b/tests/mts/bpf_existence_test.cpp
@@ -68,6 +68,8 @@
     TETHERING "map_offload_tether_upstream6_map",
     TETHERING "map_test_bitmap",
     TETHERING "map_test_tether_downstream6_map",
+    TETHERING "map_test_tether2_downstream6_map",
+    TETHERING "map_test_tether3_downstream6_map",
     TETHERING "prog_offload_schedcls_tether_downstream4_ether",
     TETHERING "prog_offload_schedcls_tether_downstream4_rawip",
     TETHERING "prog_offload_schedcls_tether_downstream6_ether",
@@ -91,6 +93,8 @@
     SHARED "map_dscpPolicy_ipv4_dscp_policies_map",
     SHARED "map_dscpPolicy_ipv6_dscp_policies_map",
     SHARED "map_dscpPolicy_socket_policy_cache_map",
+    SHARED "map_gentle_test",
+    SHARED "prog_gentle_skfilter_accept",
     NETD "map_netd_app_uid_stats_map",
     NETD "map_netd_configuration_map",
     NETD "map_netd_cookie_tag_map",
@@ -141,6 +145,27 @@
     NETD "map_netd_packet_trace_ringbuf",
 };
 
+// Provided by *current* mainline module for V+ devices
+static const set<string> MAINLINE_FOR_V_PLUS = {
+    NETD "prog_netd_connect4_inet4_connect",
+    NETD "prog_netd_connect6_inet6_connect",
+    NETD "prog_netd_recvmsg4_udp4_recvmsg",
+    NETD "prog_netd_recvmsg6_udp6_recvmsg",
+    NETD "prog_netd_sendmsg4_udp4_sendmsg",
+    NETD "prog_netd_sendmsg6_udp6_sendmsg",
+};
+
+// Provided by *current* mainline module for V+ devices with 5.4+ kernels
+static const set<string> MAINLINE_FOR_V_5_4_PLUS = {
+    NETD "prog_netd_getsockopt_prog",
+    NETD "prog_netd_setsockopt_prog",
+};
+
+// Provided by *current* mainline module for U+ devices with 5.10+ kernels
+static const set<string> MAINLINE_FOR_V_5_10_PLUS = {
+    NETD "prog_netd_cgroupsockrelease_inet_release",
+};
+
 static void addAll(set<string>& a, const set<string>& b) {
     a.insert(b.begin(), b.end());
 }
@@ -188,6 +213,9 @@
 
     // V requires Linux Kernel 4.19+, but nothing (as yet) added or removed in V.
     if (IsAtLeastV()) ASSERT_TRUE(isAtLeastKernelVersion(4, 19, 0));
+    DO_EXPECT(IsAtLeastV(), MAINLINE_FOR_V_PLUS);
+    DO_EXPECT(IsAtLeastV() && isAtLeastKernelVersion(5, 4, 0), MAINLINE_FOR_V_5_4_PLUS);
+    DO_EXPECT(IsAtLeastV() && isAtLeastKernelVersion(5, 10, 0), MAINLINE_FOR_V_5_10_PLUS);
 
     for (const auto& file : mustExist) {
         EXPECT_EQ(0, access(file.c_str(), R_OK)) << file << " does not exist";
diff --git a/tests/native/utilities/firewall.h b/tests/native/utilities/firewall.h
index b3d69bf..a5cb0b9 100644
--- a/tests/native/utilities/firewall.h
+++ b/tests/native/utilities/firewall.h
@@ -18,6 +18,7 @@
 #pragma once
 
 #include <android-base/thread_annotations.h>
+#define BPF_MAP_LOCKLESS_FOR_TEST
 #include <bpf/BpfMap.h>
 #include "netd.h"
 
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 44a8222..8526a9a 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -2178,9 +2178,8 @@
             switch (name) {
                 case ConnectivityFlags.NO_REMATCH_ALL_REQUESTS_ON_REGISTER:
                 case ConnectivityFlags.CARRIER_SERVICE_CHANGED_USE_CALLBACK:
-                    return true;
                 case ConnectivityFlags.REQUEST_RESTRICTED_WIFI:
-                    return true;
+                case ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS:
                 case KEY_DESTROY_FROZEN_SOCKETS_VERSION:
                     return true;
                 default:
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java b/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
index 7121ed4..727db58 100644
--- a/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
@@ -113,6 +113,7 @@
     private static final NetworkCapabilities CELL_CAPABILITIES = new NetworkCapabilities();
     private static final NetworkCapabilities WIFI_CAPABILITIES = new NetworkCapabilities();
     private static final NetworkCapabilities VPN_CAPABILITIES = new NetworkCapabilities();
+    private static final NetworkCapabilities BT_CAPABILITIES = new NetworkCapabilities();
     static {
         CELL_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
         CELL_CAPABILITIES.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
@@ -128,6 +129,9 @@
         VPN_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_VPN);
         VPN_CAPABILITIES.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
         VPN_CAPABILITIES.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN);
+
+        BT_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_BLUETOOTH);
+        BT_CAPABILITIES.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
     }
 
     /**
@@ -159,7 +163,9 @@
     @Mock NetworkAgentInfo mWifiNai;
     @Mock NetworkAgentInfo mCellNai;
     @Mock NetworkAgentInfo mVpnNai;
+    @Mock NetworkAgentInfo mBluetoothNai;
     @Mock NetworkInfo mNetworkInfo;
+    @Mock NetworkInfo mEmptyNetworkInfo;
     ArgumentCaptor<Notification> mCaptor;
 
     NetworkNotificationManager mManager;
@@ -174,6 +180,8 @@
         mCellNai.networkInfo = mNetworkInfo;
         mVpnNai.networkCapabilities = VPN_CAPABILITIES;
         mVpnNai.networkInfo = mNetworkInfo;
+        mBluetoothNai.networkCapabilities = BT_CAPABILITIES;
+        mBluetoothNai.networkInfo = mEmptyNetworkInfo;
         mDisplayMetrics.density = 2.275f;
         doReturn(true).when(mVpnNai).isVPN();
         doReturn(mResources).when(mCtx).getResources();
@@ -542,10 +550,11 @@
                 R.string.wifi_no_internet_detailed);
     }
 
-    private void runTelephonySignInNotificationTest(String testTitle, String testContents) {
+    private void runSignInNotificationTest(NetworkAgentInfo nai, String testTitle,
+            String testContents) {
         final int id = 101;
         final String tag = NetworkNotificationManager.tagFor(id);
-        mManager.showNotification(id, SIGN_IN, mCellNai, null, null, false);
+        mManager.showNotification(id, SIGN_IN, nai, null, null, false);
 
         final ArgumentCaptor<Notification> noteCaptor = ArgumentCaptor.forClass(Notification.class);
         verify(mNotificationManager).notify(eq(tag), eq(SIGN_IN.eventId), noteCaptor.capture());
@@ -565,7 +574,7 @@
         doReturn(testContents).when(mResources).getString(
                 R.string.mobile_network_available_no_internet_detailed, TEST_OPERATOR_NAME);
 
-        runTelephonySignInNotificationTest(testTitle, testContents);
+        runSignInNotificationTest(mCellNai, testTitle, testContents);
     }
 
     @Test
@@ -579,6 +588,21 @@
         doReturn(testContents).when(mResources).getString(
                 R.string.mobile_network_available_no_internet_detailed_unknown_carrier);
 
-        runTelephonySignInNotificationTest(testTitle, testContents);
+        runSignInNotificationTest(mCellNai, testTitle, testContents);
+    }
+
+    @Test
+    public void testBluetoothSignInNotification_EmptyNotificationContents() {
+        final String testTitle = "Test title";
+        final String testContents = "Test contents";
+        doReturn(testTitle).when(mResources).getString(
+                R.string.network_available_sign_in, 0);
+        doReturn(testContents).when(mResources).getString(
+                eq(R.string.network_available_sign_in_detailed), any());
+
+        runSignInNotificationTest(mBluetoothNai, testTitle, testContents);
+        // The details should be queried with an empty string argument. In practice the notification
+        // contents may just be an empty string, since the default translation just outputs the arg.
+        verify(mResources).getString(eq(R.string.network_available_sign_in_detailed), eq(""));
     }
 }
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSBlockedReasonsTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSBlockedReasonsTest.kt
index 0590fbb..3ad8de8 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSBlockedReasonsTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSBlockedReasonsTest.kt
@@ -387,6 +387,10 @@
                     BLOCKED_REASON_NETWORK_RESTRICTED or BLOCKED_REASON_APP_BACKGROUND
             )
         }
+        // waitForIdle since stubbing bpfNetMaps while CS handler thread calls
+        // bpfNetMaps.getNetPermForUid throws exception.
+        // ConnectivityService might haven't finished checking blocked status for all requests.
+        waitForIdle()
 
         // Disable background firewall chain
         doReturn(BLOCKED_REASON_NONE)
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkFallbackTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
similarity index 69%
rename from tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkFallbackTest.kt
rename to tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
index 9024641..88c2738 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkFallbackTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
@@ -24,6 +24,7 @@
 import android.net.NativeNetworkType
 import android.net.NetworkCapabilities
 import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED
 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED
 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
@@ -41,10 +42,13 @@
 import android.util.ArraySet
 import com.android.net.module.util.CollectionUtils
 import com.android.server.ConnectivityService.PREFERENCE_ORDER_SATELLITE_FALLBACK
+import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.TestableNetworkCallback
 import com.android.testutils.visibleOnHandlerThread
 import org.junit.Assert
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.any
@@ -61,7 +65,10 @@
 @DevSdkIgnoreRunner.MonitorThreadLeak
 @RunWith(DevSdkIgnoreRunner::class)
 @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
-class CSSatelliteNetworkPreferredTest : CSTest() {
+class CSSatelliteNetworkTest : CSTest() {
+    @get:Rule
+    val ignoreRule = DevSdkIgnoreRule()
+
     /**
      * Test createMultiLayerNrisFromSatelliteNetworkPreferredUids returns correct
      * NetworkRequestInfo.
@@ -80,54 +87,81 @@
     }
 
     /**
-     * Test that SATELLITE_NETWORK_PREFERENCE_UIDS changes will send correct net id and uid ranges
-     * to netd.
+     * Test that satellite network satisfies satellite fallback per-app default network request and
+     * send correct net id and uid ranges to netd.
      */
-    @Test
-    fun testSatelliteNetworkPreferredUidsChanged() {
+    private fun doTestSatelliteNetworkFallbackUids(restricted: Boolean) {
         val netdInOrder = inOrder(netd)
 
-        val satelliteAgent = createSatelliteAgent("satellite0")
+        val satelliteAgent = createSatelliteAgent("satellite0", restricted)
         satelliteAgent.connect()
 
         val satelliteNetId = satelliteAgent.network.netId
+        val permission = if (restricted) {INetd.PERMISSION_SYSTEM} else {INetd.PERMISSION_NONE}
         netdInOrder.verify(netd).networkCreate(
-            nativeNetworkConfigPhysical(satelliteNetId, INetd.PERMISSION_NONE))
+            nativeNetworkConfigPhysical(satelliteNetId, permission))
 
         val uid1 = PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID)
         val uid2 = PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID2)
         val uid3 = SECONDARY_USER_HANDLE.getUid(TEST_PACKAGE_UID)
 
-        // Initial satellite network preferred uids status.
-        setAndUpdateSatelliteNetworkPreferredUids(setOf())
+        // Initial satellite network fallback uids status.
+        updateSatelliteNetworkFallbackUids(setOf())
         netdInOrder.verify(netd, never()).networkAddUidRangesParcel(any())
         netdInOrder.verify(netd, never()).networkRemoveUidRangesParcel(any())
 
-        // Set SATELLITE_NETWORK_PREFERENCE_UIDS setting and verify that net id and uid ranges
-        // send to netd
+        // Update satellite network fallback uids and verify that net id and uid ranges send to netd
         var uids = mutableSetOf(uid1, uid2, uid3)
         val uidRanges1 = toUidRangeStableParcels(uidRangesForUids(uids))
         val config1 = NativeUidRangeConfig(
             satelliteNetId, uidRanges1,
             PREFERENCE_ORDER_SATELLITE_FALLBACK
         )
-        setAndUpdateSatelliteNetworkPreferredUids(uids)
+        updateSatelliteNetworkFallbackUids(uids)
         netdInOrder.verify(netd).networkAddUidRangesParcel(config1)
         netdInOrder.verify(netd, never()).networkRemoveUidRangesParcel(any())
 
-        // Set SATELLITE_NETWORK_PREFERENCE_UIDS setting again and verify that old rules are removed
-        // and new rules are added.
+        // Update satellite network fallback uids and verify that net id and uid ranges send to netd
         uids = mutableSetOf(uid1)
         val uidRanges2: Array<UidRangeParcel?> = toUidRangeStableParcels(uidRangesForUids(uids))
         val config2 = NativeUidRangeConfig(
             satelliteNetId, uidRanges2,
             PREFERENCE_ORDER_SATELLITE_FALLBACK
         )
-        setAndUpdateSatelliteNetworkPreferredUids(uids)
+        updateSatelliteNetworkFallbackUids(uids)
         netdInOrder.verify(netd).networkRemoveUidRangesParcel(config1)
         netdInOrder.verify(netd).networkAddUidRangesParcel(config2)
     }
 
+    @Test
+    fun testSatelliteNetworkFallbackUids_restricted() {
+        doTestSatelliteNetworkFallbackUids(restricted = true)
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    fun testSatelliteNetworkFallbackUids_nonRestricted() {
+        doTestSatelliteNetworkFallbackUids(restricted = false)
+    }
+
+    private fun doTestSatelliteNeverBecomeDefaultNetwork(restricted: Boolean) {
+        val agent = createSatelliteAgent("satellite0", restricted)
+        agent.connect()
+        val defaultCb = TestableNetworkCallback()
+        cm.registerDefaultNetworkCallback(defaultCb)
+        // Satellite network must not become the default network
+        defaultCb.assertNoCallback()
+    }
+
+    @Test
+    fun testSatelliteNeverBecomeDefaultNetwork_restricted() {
+        doTestSatelliteNeverBecomeDefaultNetwork(restricted = true)
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    fun testSatelliteNeverBecomeDefaultNetwork_notRestricted() {
+        doTestSatelliteNeverBecomeDefaultNetwork(restricted = false)
+    }
+
     private fun assertCreateMultiLayerNrisFromSatelliteNetworkPreferredUids(uids: Set<Int>) {
         val nris: Set<ConnectivityService.NetworkRequestInfo> =
             service.createMultiLayerNrisFromSatelliteNetworkFallbackUids(uids)
@@ -140,7 +174,7 @@
         assertEquals(PREFERENCE_ORDER_SATELLITE_FALLBACK, nri.mPreferenceOrder)
     }
 
-    private fun setAndUpdateSatelliteNetworkPreferredUids(uids: Set<Int>) {
+    private fun updateSatelliteNetworkFallbackUids(uids: Set<Int>) {
         visibleOnHandlerThread(csHandler) {
             deps.satelliteNetworkFallbackUidUpdate!!.accept(uids)
         }
@@ -150,9 +184,9 @@
         NativeNetworkConfig(netId, NativeNetworkType.PHYSICAL, permission,
             false /* secure */, VpnManager.TYPE_VPN_NONE, false /* excludeLocalRoutes */)
 
-    private fun createSatelliteAgent(name: String): CSAgentWrapper {
+    private fun createSatelliteAgent(name: String, restricted: Boolean = true): CSAgentWrapper {
         return Agent(score = keepScore(), lp = lp(name),
-            nc = nc(TRANSPORT_SATELLITE, NET_CAPABILITY_INTERNET)
+            nc = satelliteNc(restricted)
         )
     }
 
@@ -176,17 +210,19 @@
         return uidRangesForUids(*CollectionUtils.toIntArray(uids))
     }
 
-    private fun nc(transport: Int, vararg caps: Int) = NetworkCapabilities.Builder().apply {
-        addTransportType(transport)
-        caps.forEach {
-            addCapability(it)
-        }
-        // Useful capabilities for everybody
-        addCapability(NET_CAPABILITY_NOT_RESTRICTED)
-        addCapability(NET_CAPABILITY_NOT_SUSPENDED)
-        addCapability(NET_CAPABILITY_NOT_ROAMING)
-        addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
-    }.build()
+    private fun satelliteNc(restricted: Boolean) =
+            NetworkCapabilities.Builder().apply {
+                addTransportType(TRANSPORT_SATELLITE)
+
+                addCapability(NET_CAPABILITY_INTERNET)
+                addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                addCapability(NET_CAPABILITY_NOT_ROAMING)
+                addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+                if (restricted) {
+                    removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                }
+                removeCapability(NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
+            }.build()
 
     private fun lp(iface: String) = LinkProperties().apply {
         interfaceName = iface
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 47a6763..ed72fd2 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -164,6 +164,7 @@
         it[ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING] = true
         it[ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN] = true
         it[ConnectivityFlags.DELAY_DESTROY_SOCKETS] = true
+        it[ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS] = true
     }
     fun setFeatureEnabled(flag: String, enabled: Boolean) = enabledFeatures.set(flag, enabled)
 
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 9026481..7e0a225 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -56,6 +56,7 @@
 import static android.net.TrafficStats.MB_IN_BYTES;
 import static android.net.TrafficStats.UID_REMOVED;
 import static android.net.TrafficStats.UID_TETHERING;
+import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE;
 import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID;
 import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID_TAG;
 import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_XT;
@@ -64,6 +65,7 @@
 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
 import static android.text.format.DateUtils.WEEK_IN_MILLIS;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doThrow;
 import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_RAT_CHANGED;
 import static com.android.server.net.NetworkStatsEventLogger.PollEvent.pollReasonNameOf;
 import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_POLL;
@@ -101,6 +103,7 @@
 import android.app.AlarmManager;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.net.DataUsageRequest;
@@ -131,6 +134,7 @@
 import android.os.PowerManager;
 import android.os.Process;
 import android.os.SimpleClock;
+import android.os.UserHandle;
 import android.provider.Settings;
 import android.system.ErrnoException;
 import android.telephony.TelephonyManager;
@@ -255,6 +259,7 @@
     private static @Mock WifiInfo sWifiInfo;
     private @Mock INetd mNetd;
     private @Mock TetheringManager mTetheringManager;
+    private @Mock PackageManager mPm;
     private @Mock NetworkStatsFactory mStatsFactory;
     @NonNull
     private final TestNetworkStatsSettings mSettings =
@@ -306,6 +311,7 @@
     private HandlerThread mObserverHandlerThread;
     final TestDependencies mDeps = new TestDependencies();
     final HashMap<String, Boolean> mFeatureFlags = new HashMap<>();
+    final HashMap<Long, Boolean> mCompatChanges = new HashMap<>();
 
     // This will set feature flags from @FeatureFlag annotations
     // into the map before setUp() runs.
@@ -325,6 +331,16 @@
         }
 
         @Override
+        public PackageManager getPackageManager() {
+            return mPm;
+        }
+
+        @Override
+        public Context createContextAsUser(UserHandle user, int flags) {
+            return this;
+        }
+
+        @Override
         public Object getSystemService(String name) {
             if (Context.TELEPHONY_SERVICE.equals(name)) return mTelephonyManager;
             if (Context.TETHERING_SERVICE.equals(name)) return mTetheringManager;
@@ -445,6 +461,9 @@
                 any(), tetheringEventCbCaptor.capture());
         mTetheringEventCallback = tetheringEventCbCaptor.getValue();
 
+        doReturn(Process.myUid()).when(mPm)
+                .getPackageUid(eq(mServiceContext.getPackageName()), anyInt());
+
         mUsageCallback = new TestableUsageCallback(mUsageCallbackBinder);
     }
 
@@ -594,7 +613,7 @@
         }
 
         @Override
-        public boolean supportTrafficStatsRateLimitCache(Context ctx) {
+        public boolean alwaysUseTrafficStatsRateLimitCache(Context ctx) {
             return mFeatureFlags.getOrDefault(TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG, false);
         }
 
@@ -608,6 +627,14 @@
             return DEFAULT_TRAFFIC_STATS_CACHE_MAX_ENTRIES;
         }
 
+        @Override
+        public boolean isChangeEnabled(long changeId, int uid) {
+            return mCompatChanges.getOrDefault(changeId, true);
+        }
+
+        public void setChangeEnabled(long changeId, boolean enabled) {
+            mCompatChanges.put(changeId, enabled);
+        }
         @Nullable
         @Override
         public NetworkStats.Entry nativeGetTotalStat() {
@@ -1704,7 +1731,7 @@
 
         // Register and verify request and that binder was called
         DataUsageRequest request = mService.registerUsageCallback(
-                mServiceContext.getOpPackageName(), inputRequest, mUsageCallback);
+                mServiceContext.getPackageName(), inputRequest, mUsageCallback);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateWifi, request.template));
         long minThresholdInBytes = 2 * 1024 * 1024; // 2 MB
@@ -2413,17 +2440,33 @@
 
     @FeatureFlag(name = TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
     @Test
-    public void testTrafficStatsRateLimitCache_disabled() throws Exception {
-        doTestTrafficStatsRateLimitCache(false /* cacheEnabled */);
+    public void testTrafficStatsRateLimitCache_disabledWithCompatChangeEnabled() throws Exception {
+        mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, true);
+        doTestTrafficStatsRateLimitCache(true /* expectCached */);
     }
 
     @FeatureFlag(name = TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG)
     @Test
-    public void testTrafficStatsRateLimitCache_enabled() throws Exception {
-        doTestTrafficStatsRateLimitCache(true /* cacheEnabled */);
+    public void testTrafficStatsRateLimitCache_enabledWithCompatChangeEnabled() throws Exception {
+        mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, true);
+        doTestTrafficStatsRateLimitCache(true /* expectCached */);
     }
 
-    private void doTestTrafficStatsRateLimitCache(boolean cacheEnabled) throws Exception {
+    @FeatureFlag(name = TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
+    @Test
+    public void testTrafficStatsRateLimitCache_disabledWithCompatChangeDisabled() throws Exception {
+        mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, false);
+        doTestTrafficStatsRateLimitCache(false /* expectCached */);
+    }
+
+    @FeatureFlag(name = TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG)
+    @Test
+    public void testTrafficStatsRateLimitCache_enabledWithCompatChangeDisabled() throws Exception {
+        mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, false);
+        doTestTrafficStatsRateLimitCache(true /* expectCached */);
+    }
+
+    private void doTestTrafficStatsRateLimitCache(boolean expectCached) throws Exception {
         mockDefaultSettings();
         // Calling uid is not injected into the service, use the real uid to pass the caller check.
         final int myUid = Process.myUid();
@@ -2433,7 +2476,7 @@
         // Verify the values are cached.
         incrementCurrentTime(DEFAULT_TRAFFIC_STATS_CACHE_EXPIRY_DURATION_MS / 2);
         mockTrafficStatsValues(65L, 8L, 1055L, 9L);
-        if (cacheEnabled) {
+        if (expectCached) {
             assertTrafficStatsValues(TEST_IFACE, myUid, 64L, 3L, 1024L, 8L);
         } else {
             assertTrafficStatsValues(TEST_IFACE, myUid, 65L, 8L, 1055L, 9L);
@@ -2979,6 +3022,38 @@
     }
 
     @Test
+    public void testEnforcePackageNameMatchesUid() throws Exception {
+        final String testMyPackageName = "test.package.myname";
+        final String testRedPackageName = "test.package.red";
+        final String testInvalidPackageName = "test.package.notfound";
+
+        doReturn(UID_RED).when(mPm).getPackageUid(eq(testRedPackageName), anyInt());
+        doReturn(Process.myUid()).when(mPm).getPackageUid(eq(testMyPackageName), anyInt());
+        doThrow(new PackageManager.NameNotFoundException()).when(mPm)
+                .getPackageUid(eq(testInvalidPackageName), anyInt());
+
+        assertThrows(SecurityException.class, () ->
+                mService.openSessionForUsageStats(0 /* flags */, testRedPackageName));
+        assertThrows(SecurityException.class, () ->
+                mService.openSessionForUsageStats(0 /* flags */, testInvalidPackageName));
+        assertThrows(NullPointerException.class, () ->
+                mService.openSessionForUsageStats(0 /* flags */, null));
+        // Verify package name belongs to ourselves does not throw.
+        mService.openSessionForUsageStats(0 /* flags */, testMyPackageName);
+
+        long thresholdInBytes = 10 * 1024 * 1024;  // 10 MB
+        DataUsageRequest request = new DataUsageRequest(
+                2 /* requestId */, sTemplateImsi1, thresholdInBytes);
+        assertThrows(SecurityException.class, () ->
+                mService.registerUsageCallback(testRedPackageName, request, mUsageCallback));
+        assertThrows(SecurityException.class, () ->
+                mService.registerUsageCallback(testInvalidPackageName, request, mUsageCallback));
+        assertThrows(NullPointerException.class, () ->
+                mService.registerUsageCallback(null, request, mUsageCallback));
+        mService.registerUsageCallback(testMyPackageName, request, mUsageCallback);
+    }
+
+    @Test
     public void testDumpSkDestroyListenerLogs() throws ErrnoException {
         doAnswer((invocation) -> {
             final IndentingPrintWriter ipw = (IndentingPrintWriter) invocation.getArgument(0);
diff --git a/thread/README.md b/thread/README.md
index f50e0cd..41b73ac 100644
--- a/thread/README.md
+++ b/thread/README.md
@@ -1,3 +1,18 @@
 # Thread
 
 Bring the [Thread](https://www.threadgroup.org/) networking protocol to Android.
+
+## Try Thread with Cuttlefish
+
+```
+# Get the code and go to the Android source code root directory
+
+source build/envsetup.sh
+lunch aosp_cf_x86_64_phone-trunk_staging-userdebug
+m
+
+launch_cvd
+```
+
+Open `https://localhost:8443/` in your web browser, you can find the Thread
+demoapp (with the Thread logo) in the cuttlefish instance. Open it and have fun with Thread!
diff --git a/thread/service/java/com/android/server/thread/InfraInterfaceController.java b/thread/service/java/com/android/server/thread/InfraInterfaceController.java
index be54cbc..e72c9ee 100644
--- a/thread/service/java/com/android/server/thread/InfraInterfaceController.java
+++ b/thread/service/java/com/android/server/thread/InfraInterfaceController.java
@@ -16,14 +16,30 @@
 
 package com.android.server.thread;
 
-import android.os.ParcelFileDescriptor;
+import static android.system.OsConstants.IPPROTO_IPV6;
+import static android.system.OsConstants.IPPROTO_RAW;
+import static android.system.OsConstants.IPV6_CHECKSUM;
+import static android.system.OsConstants.IPV6_MULTICAST_HOPS;
+import static android.system.OsConstants.IPV6_RECVHOPLIMIT;
+import static android.system.OsConstants.IPV6_RECVPKTINFO;
+import static android.system.OsConstants.IPV6_UNICAST_HOPS;
 
+import android.net.util.SocketUtils;
+import android.os.ParcelFileDescriptor;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import java.io.FileDescriptor;
 import java.io.IOException;
 
 /** Controller for the infrastructure network interface. */
 public class InfraInterfaceController {
     private static final String TAG = "InfraIfController";
 
+    private static final int ENABLE = 1;
+    private static final int IPV6_CHECKSUM_OFFSET = 2;
+    private static final int HOP_LIMIT = 255;
+
     static {
         System.loadLibrary("service-thread-jni");
     }
@@ -37,8 +53,21 @@
      * @throws IOException when fails to create the socket.
      */
     public ParcelFileDescriptor createIcmp6Socket(String infraInterfaceName) throws IOException {
-        return ParcelFileDescriptor.adoptFd(nativeCreateIcmp6Socket(infraInterfaceName));
+        ParcelFileDescriptor parcelFd =
+                ParcelFileDescriptor.adoptFd(nativeCreateFilteredIcmp6Socket());
+        FileDescriptor fd = parcelFd.getFileDescriptor();
+        try {
+            Os.setsockoptInt(fd, IPPROTO_RAW, IPV6_CHECKSUM, IPV6_CHECKSUM_OFFSET);
+            Os.setsockoptInt(fd, IPPROTO_IPV6, IPV6_RECVPKTINFO, ENABLE);
+            Os.setsockoptInt(fd, IPPROTO_IPV6, IPV6_RECVHOPLIMIT, ENABLE);
+            Os.setsockoptInt(fd, IPPROTO_IPV6, IPV6_UNICAST_HOPS, HOP_LIMIT);
+            Os.setsockoptInt(fd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, HOP_LIMIT);
+            SocketUtils.bindSocketToInterface(fd, infraInterfaceName);
+        } catch (ErrnoException e) {
+            throw new IOException("Failed to setsockopt for the ICMPv6 socket", e);
+        }
+        return parcelFd;
     }
 
-    private static native int nativeCreateIcmp6Socket(String interfaceName) throws IOException;
+    private static native int nativeCreateFilteredIcmp6Socket() throws IOException;
 }
diff --git a/thread/service/java/com/android/server/thread/NsdPublisher.java b/thread/service/java/com/android/server/thread/NsdPublisher.java
index 2c14f1d..1447ff8 100644
--- a/thread/service/java/com/android/server/thread/NsdPublisher.java
+++ b/thread/service/java/com/android/server/thread/NsdPublisher.java
@@ -19,11 +19,15 @@
 import static android.net.nsd.NsdManager.PROTOCOL_DNS_SD;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.Context;
+import android.net.DnsResolver;
 import android.net.InetAddresses;
+import android.net.Network;
 import android.net.nsd.DiscoveryRequest;
 import android.net.nsd.NsdManager;
 import android.net.nsd.NsdServiceInfo;
+import android.os.CancellationSignal;
 import android.os.Handler;
 import android.os.RemoteException;
 import android.text.TextUtils;
@@ -34,13 +38,14 @@
 import com.android.server.thread.openthread.DnsTxtAttribute;
 import com.android.server.thread.openthread.INsdDiscoverServiceCallback;
 import com.android.server.thread.openthread.INsdPublisher;
+import com.android.server.thread.openthread.INsdResolveHostCallback;
 import com.android.server.thread.openthread.INsdResolveServiceCallback;
 import com.android.server.thread.openthread.INsdStatusReceiver;
 
 import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -56,24 +61,36 @@
  * {@code mHandler} itself.
  */
 public final class NsdPublisher extends INsdPublisher.Stub {
-    // TODO: b/321883491 - specify network for mDNS operations
     private static final String TAG = NsdPublisher.class.getSimpleName();
+
+    // TODO: b/321883491 - specify network for mDNS operations
+    @Nullable private Network mNetwork;
     private final NsdManager mNsdManager;
+    private final DnsResolver mDnsResolver;
     private final Handler mHandler;
     private final Executor mExecutor;
     private final SparseArray<RegistrationListener> mRegistrationListeners = new SparseArray<>(0);
     private final SparseArray<DiscoveryListener> mDiscoveryListeners = new SparseArray<>(0);
     private final SparseArray<ServiceInfoListener> mServiceInfoListeners = new SparseArray<>(0);
+    private final SparseArray<HostInfoListener> mHostInfoListeners = new SparseArray<>(0);
 
     @VisibleForTesting
-    public NsdPublisher(NsdManager nsdManager, Handler handler) {
+    public NsdPublisher(NsdManager nsdManager, DnsResolver dnsResolver, Handler handler) {
+        mNetwork = null;
         mNsdManager = nsdManager;
+        mDnsResolver = dnsResolver;
         mHandler = handler;
         mExecutor = runnable -> mHandler.post(runnable);
     }
 
     public static NsdPublisher newInstance(Context context, Handler handler) {
-        return new NsdPublisher(context.getSystemService(NsdManager.class), handler);
+        return new NsdPublisher(
+                context.getSystemService(NsdManager.class), DnsResolver.getInstance(), handler);
+    }
+
+    // TODO: b/321883491 - NsdPublisher should be disabled when mNetwork is null
+    public void setNetworkForHostResolution(@Nullable Network network) {
+        mNetwork = network;
     }
 
     @Override
@@ -291,6 +308,53 @@
         }
     }
 
+    @Override
+    public void resolveHost(String name, INsdResolveHostCallback callback, int listenerId) {
+        mHandler.post(() -> resolveHostInternal(name, callback, listenerId));
+    }
+
+    private void resolveHostInternal(
+            String name, INsdResolveHostCallback callback, int listenerId) {
+        checkOnHandlerThread();
+
+        String fullHostname = name + ".local";
+        CancellationSignal cancellationSignal = new CancellationSignal();
+        HostInfoListener listener =
+                new HostInfoListener(name, callback, cancellationSignal, listenerId);
+        mDnsResolver.query(
+                mNetwork,
+                fullHostname,
+                DnsResolver.FLAG_NO_CACHE_LOOKUP,
+                mExecutor,
+                cancellationSignal,
+                listener);
+        mHostInfoListeners.append(listenerId, listener);
+
+        Log.i(TAG, "Resolving host." + " Listener ID: " + listenerId + ", hostname: " + name);
+    }
+
+    @Override
+    public void stopHostResolution(int listenerId) {
+        mHandler.post(() -> stopHostResolutionInternal(listenerId));
+    }
+
+    private void stopHostResolutionInternal(int listenerId) {
+        checkOnHandlerThread();
+
+        HostInfoListener listener = mHostInfoListeners.get(listenerId);
+        if (listener == null) {
+            Log.w(
+                    TAG,
+                    "Failed to stop host resolution. Listener ID: "
+                            + listenerId
+                            + ". The listener is null.");
+            return;
+        }
+        Log.i(TAG, "Stopping host resolution. Listener: " + listener);
+        listener.cancel();
+        mHostInfoListeners.remove(listenerId);
+    }
+
     private void checkOnHandlerThread() {
         if (mHandler.getLooper().getThread() != Thread.currentThread()) {
             throw new IllegalStateException(
@@ -550,9 +614,8 @@
             }
             List<DnsTxtAttribute> txtList = new ArrayList<>();
             for (Map.Entry<String, byte[]> entry : serviceInfo.getAttributes().entrySet()) {
-                DnsTxtAttribute attribute = new DnsTxtAttribute();
-                attribute.name = entry.getKey();
-                attribute.value = Arrays.copyOf(entry.getValue(), entry.getValue().length);
+                DnsTxtAttribute attribute =
+                        new DnsTxtAttribute(entry.getKey(), entry.getValue().clone());
                 txtList.add(attribute);
             }
             // TODO: b/329018320 - Use the serviceInfo.getExpirationTime to derive TTL.
@@ -586,4 +649,78 @@
             return "ID: " + mListenerId + ", service name: " + mName + ", service type: " + mType;
         }
     }
+
+    class HostInfoListener implements DnsResolver.Callback<List<InetAddress>> {
+        private final String mHostname;
+        private final INsdResolveHostCallback mResolveHostCallback;
+        private final CancellationSignal mCancellationSignal;
+        private final int mListenerId;
+
+        HostInfoListener(
+                @NonNull String hostname,
+                INsdResolveHostCallback resolveHostCallback,
+                CancellationSignal cancellationSignal,
+                int listenerId) {
+            this.mHostname = hostname;
+            this.mResolveHostCallback = resolveHostCallback;
+            this.mCancellationSignal = cancellationSignal;
+            this.mListenerId = listenerId;
+        }
+
+        @Override
+        public void onAnswer(@NonNull List<InetAddress> answerList, int rcode) {
+            checkOnHandlerThread();
+
+            Log.i(
+                    TAG,
+                    "Host is resolved."
+                            + " Listener ID: "
+                            + mListenerId
+                            + ", hostname: "
+                            + mHostname
+                            + ", addresses: "
+                            + answerList
+                            + ", return code: "
+                            + rcode);
+            List<String> addressStrings = new ArrayList<>();
+            for (InetAddress address : answerList) {
+                addressStrings.add(address.getHostAddress());
+            }
+            try {
+                mResolveHostCallback.onHostResolved(mHostname, addressStrings);
+            } catch (RemoteException e) {
+                // do nothing if the client is dead
+            }
+            mHostInfoListeners.remove(mListenerId);
+        }
+
+        @Override
+        public void onError(@NonNull DnsResolver.DnsException error) {
+            checkOnHandlerThread();
+
+            Log.i(
+                    TAG,
+                    "Failed to resolve host."
+                            + " Listener ID: "
+                            + mListenerId
+                            + ", hostname: "
+                            + mHostname,
+                    error);
+            try {
+                mResolveHostCallback.onHostResolved(mHostname, Collections.emptyList());
+            } catch (RemoteException e) {
+                // do nothing if the client is dead
+            }
+            mHostInfoListeners.remove(mListenerId);
+        }
+
+        public String toString() {
+            return "ID: " + mListenerId + ", hostname: " + mHostname;
+        }
+
+        void cancel() {
+            mCancellationSignal.cancel();
+            mHostInfoListeners.remove(mListenerId);
+        }
+    }
 }
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index af9abdf..0c77dee 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -61,6 +61,8 @@
 import static com.android.server.thread.openthread.IOtDaemon.OT_STATE_ENABLED;
 import static com.android.server.thread.openthread.IOtDaemon.TUN_IF_NAME;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import android.Manifest.permission;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -117,6 +119,7 @@
 import com.android.server.connectivity.ConnectivityResources;
 import com.android.server.thread.openthread.BackboneRouterState;
 import com.android.server.thread.openthread.BorderRouterConfigurationParcel;
+import com.android.server.thread.openthread.DnsTxtAttribute;
 import com.android.server.thread.openthread.IChannelMasksReceiver;
 import com.android.server.thread.openthread.IOtDaemon;
 import com.android.server.thread.openthread.IOtDaemonCallback;
@@ -130,7 +133,6 @@
 
 import java.io.IOException;
 import java.net.Inet6Address;
-import java.nio.charset.StandardCharsets;
 import java.security.SecureRandom;
 import java.time.Clock;
 import java.time.DateTimeException;
@@ -338,9 +340,11 @@
         final String modelName = resources.getString(R.string.config_thread_model_name);
         final String vendorName = resources.getString(R.string.config_thread_vendor_name);
         final String vendorOui = resources.getString(R.string.config_thread_vendor_oui);
+        final boolean managedByGoogle =
+                resources.getBoolean(R.bool.config_thread_managed_by_google_home);
 
         if (!modelName.isEmpty()) {
-            if (modelName.getBytes(StandardCharsets.UTF_8).length > MAX_MODEL_NAME_UTF8_BYTES) {
+            if (modelName.getBytes(UTF_8).length > MAX_MODEL_NAME_UTF8_BYTES) {
                 throw new IllegalStateException(
                         "Model name is longer than "
                                 + MAX_MODEL_NAME_UTF8_BYTES
@@ -350,7 +354,7 @@
         }
 
         if (!vendorName.isEmpty()) {
-            if (vendorName.getBytes(StandardCharsets.UTF_8).length > MAX_VENDOR_NAME_UTF8_BYTES) {
+            if (vendorName.getBytes(UTF_8).length > MAX_VENDOR_NAME_UTF8_BYTES) {
                 throw new IllegalStateException(
                         "Vendor name is longer than "
                                 + MAX_VENDOR_NAME_UTF8_BYTES
@@ -367,9 +371,21 @@
         meshcopTxts.modelName = modelName;
         meshcopTxts.vendorName = vendorName;
         meshcopTxts.vendorOui = HexEncoding.decode(vendorOui.replace("-", "").replace(":", ""));
+        meshcopTxts.nonStandardTxtEntries = List.of(makeManagedByGoogleTxtAttr(managedByGoogle));
+
         return meshcopTxts;
     }
 
+    /**
+     * Creates a DNS-SD TXT entry for indicating whether Thread on this device is managed by Google.
+     *
+     * @return TXT entry "vgh=1" if {@code managedByGoogle} is {@code true}; otherwise, "vgh=0"
+     */
+    private static DnsTxtAttribute makeManagedByGoogleTxtAttr(boolean managedByGoogle) {
+        final byte[] value = (managedByGoogle ? "1" : "0").getBytes(UTF_8);
+        return new DnsTxtAttribute("vgh", value);
+    }
+
     private void onOtDaemonDied() {
         checkOnHandlerThread();
         Log.w(TAG, "OT daemon is dead, clean up...");
@@ -717,6 +733,7 @@
                 if (mNetworkToInterface.containsKey(mUpstreamNetwork)) {
                     enableBorderRouting(mNetworkToInterface.get(mUpstreamNetwork));
                 }
+                mNsdPublisher.setNetworkForHostResolution(mUpstreamNetwork);
             }
         }
     }
diff --git a/thread/service/jni/com_android_server_thread_InfraInterfaceController.cpp b/thread/service/jni/com_android_server_thread_InfraInterfaceController.cpp
index 5d24eab..1f260f2 100644
--- a/thread/service/jni/com_android_server_thread_InfraInterfaceController.cpp
+++ b/thread/service/jni/com_android_server_thread_InfraInterfaceController.cpp
@@ -42,15 +42,8 @@
 
 namespace android {
 static jint
-com_android_server_thread_InfraInterfaceController_createIcmp6Socket(JNIEnv *env, jobject clazz,
-                                                                     jstring interfaceName) {
-  ScopedUtfChars ifName(env, interfaceName);
-
-  struct icmp6_filter filter;
-  constexpr int kEnable = 1;
-  constexpr int kIpv6ChecksumOffset = 2;
-  constexpr int kHopLimit = 255;
-
+com_android_server_thread_InfraInterfaceController_createFilteredIcmp6Socket(JNIEnv *env,
+                                                                             jobject clazz) {
   // Initializes the ICMPv6 socket.
   int sock = socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
   if (sock == -1) {
@@ -59,6 +52,7 @@
     return -1;
   }
 
+  struct icmp6_filter filter;
   // Only accept Router Advertisements, Router Solicitations and Neighbor
   // Advertisements.
   ICMP6_FILTER_SETBLOCKALL(&filter);
@@ -73,53 +67,6 @@
     return -1;
   }
 
-  // We want a source address and interface index.
-
-  if (setsockopt(sock, IPPROTO_IPV6, IPV6_RECVPKTINFO, &kEnable, sizeof(kEnable)) != 0) {
-    jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt IPV6_RECVPKTINFO (%s)",
-                         strerror(errno));
-    close(sock);
-    return -1;
-  }
-
-  if (setsockopt(sock, IPPROTO_RAW, IPV6_CHECKSUM, &kIpv6ChecksumOffset,
-                 sizeof(kIpv6ChecksumOffset)) != 0) {
-    jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt IPV6_CHECKSUM (%s)",
-                         strerror(errno));
-    close(sock);
-    return -1;
-  }
-
-  // We need to be able to reject RAs arriving from off-link.
-  if (setsockopt(sock, IPPROTO_IPV6, IPV6_RECVHOPLIMIT, &kEnable, sizeof(kEnable)) != 0) {
-    jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt IPV6_RECVHOPLIMIT (%s)",
-                         strerror(errno));
-    close(sock);
-    return -1;
-  }
-
-  if (setsockopt(sock, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &kHopLimit, sizeof(kHopLimit)) != 0) {
-    jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt IPV6_UNICAST_HOPS (%s)",
-                         strerror(errno));
-    close(sock);
-    return -1;
-  }
-
-  if (setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &kHopLimit, sizeof(kHopLimit)) != 0) {
-    jniThrowExceptionFmt(env, "java/io/IOException",
-                         "failed to create the setsockopt IPV6_MULTICAST_HOPS (%s)",
-                         strerror(errno));
-    close(sock);
-    return -1;
-  }
-
-  if (setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, ifName.c_str(), strlen(ifName.c_str()))) {
-    jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt SO_BINDTODEVICE (%s)",
-                         strerror(errno));
-    close(sock);
-    return -1;
-  }
-
   return sock;
 }
 
@@ -129,8 +76,8 @@
 
 static const JNINativeMethod gMethods[] = {
     /* name, signature, funcPtr */
-    {"nativeCreateIcmp6Socket", "(Ljava/lang/String;)I",
-     (void *)com_android_server_thread_InfraInterfaceController_createIcmp6Socket},
+    {"nativeCreateFilteredIcmp6Socket", "()I",
+     (void *)com_android_server_thread_InfraInterfaceController_createFilteredIcmp6Socket},
 };
 
 int register_com_android_server_thread_InfraInterfaceController(JNIEnv *env) {
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
index 8cdf38d..c1cf0a0 100644
--- a/thread/tests/cts/Android.bp
+++ b/thread/tests/cts/Android.bp
@@ -21,6 +21,7 @@
 
 android_test {
     name: "CtsThreadNetworkTestCases",
+    defaults: ["cts_defaults"],
     min_sdk_version: "33",
     sdk_version: "test_current",
     manifest: "AndroidManifest.xml",
diff --git a/thread/tests/cts/AndroidTest.xml b/thread/tests/cts/AndroidTest.xml
index ffc181c..6eda1e9 100644
--- a/thread/tests/cts/AndroidTest.xml
+++ b/thread/tests/cts/AndroidTest.xml
@@ -38,6 +38,11 @@
         <option name="mainline-module-package-name" value="com.google.android.tethering" />
     </object>
 
+    <object type="module_controller"
+            class="com.android.tradefed.testtype.suite.module.DeviceFeatureModuleController">
+        <option name="required-feature" value="android.hardware.thread_network" />
+    </object>
+
     <!-- Install test -->
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="test-file-name" value="CtsThreadNetworkTestCases.apk" />
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
index 11c4819..0e95703 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -45,7 +45,6 @@
 
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.fail;
-
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 
 import android.content.Context;
@@ -859,6 +858,7 @@
         assertThat(txtMap.get("rv")).isNotNull();
         assertThat(txtMap.get("tv")).isNotNull();
         assertThat(txtMap.get("sb")).isNotNull();
+        assertThat(new String(txtMap.get("vgh"))).isIn(List.of("0", "1"));
     }
 
     @Test
@@ -885,6 +885,7 @@
         assertThat(txtMap.get("tv")).isNotNull();
         assertThat(txtMap.get("sb")).isNotNull();
         assertThat(txtMap.get("id").length).isEqualTo(16);
+        assertThat(new String(txtMap.get("vgh"))).isIn(List.of("0", "1"));
     }
 
     @Test
diff --git a/thread/tests/integration/AndroidTest.xml b/thread/tests/integration/AndroidTest.xml
index 152c1c3..8f98941 100644
--- a/thread/tests/integration/AndroidTest.xml
+++ b/thread/tests/integration/AndroidTest.xml
@@ -31,6 +31,11 @@
         <option name="mainline-module-package-name" value="com.google.android.tethering" />
     </object>
 
+    <object type="module_controller"
+            class="com.android.tradefed.testtype.suite.module.DeviceFeatureModuleController">
+        <option name="required-feature" value="android.hardware.thread_network" />
+    </object>
+
     <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
 
     <!-- Install test -->
diff --git a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
index 46cf562..c0a8eea 100644
--- a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
+++ b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
@@ -20,10 +20,14 @@
 
 import static com.google.common.io.BaseEncoding.base16;
 
+import static java.util.concurrent.TimeUnit.SECONDS;
+
 import android.net.InetAddresses;
 import android.net.IpPrefix;
 import android.net.nsd.NsdServiceInfo;
 import android.net.thread.ActiveOperationalDataset;
+import android.os.Handler;
+import android.os.HandlerThread;
 
 import com.google.errorprone.annotations.FormatMethod;
 
@@ -39,6 +43,8 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeoutException;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -60,10 +66,13 @@
     private static final float PING_TIMEOUT_0_1_SECOND = 0.1f;
     // 1 second timeout should be used when response is expected.
     private static final float PING_TIMEOUT_1_SECOND = 1f;
+    private static final int READ_LINE_TIMEOUT_SECONDS = 5;
 
     private final Process mProcess;
     private final BufferedReader mReader;
     private final BufferedWriter mWriter;
+    private final HandlerThread mReaderHandlerThread;
+    private final Handler mReaderHandler;
 
     private ActiveOperationalDataset mActiveOperationalDataset;
 
@@ -87,11 +96,15 @@
         }
         mReader = new BufferedReader(new InputStreamReader(mProcess.getInputStream()));
         mWriter = new BufferedWriter(new OutputStreamWriter(mProcess.getOutputStream()));
+        mReaderHandlerThread = new HandlerThread("FullThreadDeviceReader");
+        mReaderHandlerThread.start();
+        mReaderHandler = new Handler(mReaderHandlerThread.getLooper());
         mActiveOperationalDataset = null;
     }
 
     public void destroy() {
         mProcess.destroy();
+        mReaderHandlerThread.quit();
     }
 
     /**
@@ -213,7 +226,7 @@
     public String udpReceive() throws IOException {
         Pattern pattern =
                 Pattern.compile("> (\\d+) bytes from ([\\da-f:]+) (\\d+) ([\\x00-\\x7F]+)");
-        Matcher matcher = pattern.matcher(mReader.readLine());
+        Matcher matcher = pattern.matcher(readLine());
         matcher.matches();
 
         return matcher.group(4);
@@ -500,10 +513,27 @@
         }
     }
 
+    private String readLine() throws IOException {
+        final CompletableFuture<String> future = new CompletableFuture<>();
+        mReaderHandler.post(
+                () -> {
+                    try {
+                        future.complete(mReader.readLine());
+                    } catch (IOException e) {
+                        future.completeExceptionally(e);
+                    }
+                });
+        try {
+            return future.get(READ_LINE_TIMEOUT_SECONDS, SECONDS);
+        } catch (InterruptedException | ExecutionException | TimeoutException e) {
+            throw new IOException("Failed to read a line from ot-cli-ftd");
+        }
+    }
+
     private List<String> readUntilDone() throws IOException {
         ArrayList<String> result = new ArrayList<>();
         String line;
-        while ((line = mReader.readLine()) != null) {
+        while ((line = readLine()) != null) {
             if (line.equals("Done")) {
                 break;
             }
diff --git a/thread/tests/unit/AndroidTest.xml b/thread/tests/unit/AndroidTest.xml
index d16e423..58e9bdd 100644
--- a/thread/tests/unit/AndroidTest.xml
+++ b/thread/tests/unit/AndroidTest.xml
@@ -31,6 +31,11 @@
         <option name="mainline-module-package-name" value="com.google.android.tethering" />
     </object>
 
+    <object type="module_controller"
+            class="com.android.tradefed.testtype.suite.module.DeviceFeatureModuleController">
+        <option name="required-feature" value="android.hardware.thread_network" />
+    </object>
+
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="test-file-name" value="ThreadNetworkUnitTests.apk" />
         <option name="check-min-sdk" value="true" />
diff --git a/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java b/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
index 8886c73..b32986d 100644
--- a/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
@@ -16,56 +16,73 @@
 
 package com.android.server.thread;
 
+import static android.net.DnsResolver.ERROR_SYSTEM;
 import static android.net.nsd.NsdManager.FAILURE_INTERNAL_ERROR;
 import static android.net.nsd.NsdManager.PROTOCOL_DNS_SD;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.hamcrest.Matchers.containsInAnyOrder;
 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.doThrow;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.hamcrest.MockitoHamcrest.argThat;
 
+import android.net.DnsResolver;
 import android.net.InetAddresses;
+import android.net.Network;
 import android.net.nsd.DiscoveryRequest;
 import android.net.nsd.NsdManager;
 import android.net.nsd.NsdServiceInfo;
+import android.os.CancellationSignal;
 import android.os.Handler;
 import android.os.test.TestLooper;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
 import com.android.server.thread.openthread.DnsTxtAttribute;
 import com.android.server.thread.openthread.INsdDiscoverServiceCallback;
+import com.android.server.thread.openthread.INsdResolveHostCallback;
 import com.android.server.thread.openthread.INsdResolveServiceCallback;
 import com.android.server.thread.openthread.INsdStatusReceiver;
 
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
-import org.mockito.ArgumentMatcher;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
 import java.net.InetAddress;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
-import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.Executor;
 
 /** Unit tests for {@link NsdPublisher}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
 public final class NsdPublisherTest {
+    private static final DnsTxtAttribute TEST_TXT_ENTRY_1 =
+            new DnsTxtAttribute("key1", new byte[] {0x01, 0x02});
+    private static final DnsTxtAttribute TEST_TXT_ENTRY_2 =
+            new DnsTxtAttribute("key2", new byte[] {0x03});
+
     @Mock private NsdManager mMockNsdManager;
+    @Mock private DnsResolver mMockDnsResolver;
 
     @Mock private INsdStatusReceiver mRegistrationReceiver;
     @Mock private INsdStatusReceiver mUnregistrationReceiver;
     @Mock private INsdDiscoverServiceCallback mDiscoverServiceCallback;
     @Mock private INsdResolveServiceCallback mResolveServiceCallback;
+    @Mock private INsdResolveHostCallback mResolveHostCallback;
+    @Mock private Network mNetwork;
 
     private TestLooper mTestLooper;
     private NsdPublisher mNsdPublisher;
@@ -79,19 +96,15 @@
     public void registerService_nsdManagerSucceeds_serviceRegistrationSucceeds() throws Exception {
         prepareTest();
 
-        DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
-        DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
-
         mNsdPublisher.registerService(
                 null,
                 "MyService",
                 "_test._tcp",
                 List.of("_subtype1", "_subtype2"),
                 12345,
-                List.of(txt1, txt2),
+                List.of(TEST_TXT_ENTRY_1, TEST_TXT_ENTRY_2),
                 mRegistrationReceiver,
                 16 /* listenerId */);
-
         mTestLooper.dispatchAll();
 
         ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
@@ -118,11 +131,10 @@
         assertThat(actualServiceInfo.getSubtypes()).isEqualTo(Set.of("_subtype1", "_subtype2"));
         assertThat(actualServiceInfo.getPort()).isEqualTo(12345);
         assertThat(actualServiceInfo.getAttributes().size()).isEqualTo(2);
-        assertThat(actualServiceInfo.getAttributes().get("key1"))
-                .isEqualTo(new byte[] {(byte) 0x01, (byte) 0x02});
-        assertThat(actualServiceInfo.getAttributes().get("key2"))
-                .isEqualTo(new byte[] {(byte) 0x03});
-
+        assertThat(actualServiceInfo.getAttributes().get(TEST_TXT_ENTRY_1.name))
+                .isEqualTo(TEST_TXT_ENTRY_1.value);
+        assertThat(actualServiceInfo.getAttributes().get(TEST_TXT_ENTRY_2.name))
+                .isEqualTo(TEST_TXT_ENTRY_2.value);
         verify(mRegistrationReceiver, times(1)).onSuccess();
     }
 
@@ -130,19 +142,15 @@
     public void registerService_nsdManagerFails_serviceRegistrationFails() throws Exception {
         prepareTest();
 
-        DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
-        DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
-
         mNsdPublisher.registerService(
                 null,
                 "MyService",
                 "_test._tcp",
                 List.of("_subtype1", "_subtype2"),
                 12345,
-                List.of(txt1, txt2),
+                List.of(TEST_TXT_ENTRY_1, TEST_TXT_ENTRY_2),
                 mRegistrationReceiver,
                 16 /* listenerId */);
-
         mTestLooper.dispatchAll();
 
         ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
@@ -169,21 +177,16 @@
         assertThat(actualServiceInfo.getSubtypes()).isEqualTo(Set.of("_subtype1", "_subtype2"));
         assertThat(actualServiceInfo.getPort()).isEqualTo(12345);
         assertThat(actualServiceInfo.getAttributes().size()).isEqualTo(2);
-        assertThat(actualServiceInfo.getAttributes().get("key1"))
-                .isEqualTo(new byte[] {(byte) 0x01, (byte) 0x02});
-        assertThat(actualServiceInfo.getAttributes().get("key2"))
-                .isEqualTo(new byte[] {(byte) 0x03});
-
+        assertThat(actualServiceInfo.getAttributes().get(TEST_TXT_ENTRY_1.name))
+                .isEqualTo(TEST_TXT_ENTRY_1.value);
+        assertThat(actualServiceInfo.getAttributes().get(TEST_TXT_ENTRY_2.name))
+                .isEqualTo(TEST_TXT_ENTRY_2.value);
         verify(mRegistrationReceiver, times(1)).onError(FAILURE_INTERNAL_ERROR);
     }
 
     @Test
     public void registerService_nsdManagerThrows_serviceRegistrationFails() throws Exception {
         prepareTest();
-
-        DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
-        DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
-
         doThrow(new IllegalArgumentException("NsdManager fails"))
                 .when(mMockNsdManager)
                 .registerService(any(), anyInt(), any(Executor.class), any());
@@ -194,7 +197,7 @@
                 "_test._tcp",
                 List.of("_subtype1", "_subtype2"),
                 12345,
-                List.of(txt1, txt2),
+                List.of(TEST_TXT_ENTRY_1, TEST_TXT_ENTRY_2),
                 mRegistrationReceiver,
                 16 /* listenerId */);
         mTestLooper.dispatchAll();
@@ -207,16 +210,13 @@
             throws Exception {
         prepareTest();
 
-        DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
-        DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
-
         mNsdPublisher.registerService(
                 null,
                 "MyService",
                 "_test._tcp",
                 List.of("_subtype1", "_subtype2"),
                 12345,
-                List.of(txt1, txt2),
+                List.of(TEST_TXT_ENTRY_1, TEST_TXT_ENTRY_2),
                 mRegistrationReceiver,
                 16 /* listenerId */);
 
@@ -252,16 +252,13 @@
     public void unregisterService_nsdManagerFails_serviceUnregistrationFails() throws Exception {
         prepareTest();
 
-        DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
-        DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
-
         mNsdPublisher.registerService(
                 null,
                 "MyService",
                 "_test._tcp",
                 List.of("_subtype1", "_subtype2"),
                 12345,
-                List.of(txt1, txt2),
+                List.of(TEST_TXT_ENTRY_1, TEST_TXT_ENTRY_2),
                 mRegistrationReceiver,
                 16 /* listenerId */);
 
@@ -579,8 +576,8 @@
                 List.of(
                         InetAddress.parseNumericAddress("2001::1"),
                         InetAddress.parseNumericAddress("2001::2")));
-        serviceInfo.setAttribute("key1", new byte[] {(byte) 0x01, (byte) 0x02});
-        serviceInfo.setAttribute("key2", new byte[] {(byte) 0x03});
+        serviceInfo.setAttribute(TEST_TXT_ENTRY_1.name, TEST_TXT_ENTRY_1.value);
+        serviceInfo.setAttribute(TEST_TXT_ENTRY_2.name, TEST_TXT_ENTRY_2.value);
         serviceInfoCallbackArgumentCaptor.getValue().onServiceUpdated(serviceInfo);
         mTestLooper.dispatchAll();
 
@@ -591,11 +588,8 @@
                         eq("_test._tcp"),
                         eq(12345),
                         eq(List.of("2001::1", "2001::2")),
-                        argThat(
-                                new TxtMatcher(
-                                        List.of(
-                                                makeTxtAttribute("key1", List.of(0x01, 0x02)),
-                                                makeTxtAttribute("key2", List.of(0x03))))),
+                        (List<DnsTxtAttribute>)
+                                argThat(containsInAnyOrder(TEST_TXT_ENTRY_1, TEST_TXT_ENTRY_2)),
                         anyInt());
     }
 
@@ -637,12 +631,86 @@
     }
 
     @Test
-    public void reset_unregisterAll() {
+    public void resolveHost_hostResolved() throws Exception {
         prepareTest();
 
-        DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
-        DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
+        mNsdPublisher.resolveHost("test", mResolveHostCallback, 10 /* listenerId */);
+        mTestLooper.dispatchAll();
 
+        ArgumentCaptor<DnsResolver.Callback<List<InetAddress>>> resolveHostCallbackArgumentCaptor =
+                ArgumentCaptor.forClass(DnsResolver.Callback.class);
+        verify(mMockDnsResolver, times(1))
+                .query(
+                        eq(mNetwork),
+                        eq("test.local"),
+                        eq(DnsResolver.FLAG_NO_CACHE_LOOKUP),
+                        any(Executor.class),
+                        any(CancellationSignal.class),
+                        resolveHostCallbackArgumentCaptor.capture());
+        resolveHostCallbackArgumentCaptor
+                .getValue()
+                .onAnswer(
+                        List.of(
+                                InetAddresses.parseNumericAddress("2001::1"),
+                                InetAddresses.parseNumericAddress("2001::2")),
+                        0);
+        mTestLooper.dispatchAll();
+
+        verify(mResolveHostCallback, times(1))
+                .onHostResolved("test", List.of("2001::1", "2001::2"));
+    }
+
+    @Test
+    public void resolveHost_errorReported() throws Exception {
+        prepareTest();
+
+        mNsdPublisher.resolveHost("test", mResolveHostCallback, 10 /* listenerId */);
+        mTestLooper.dispatchAll();
+
+        ArgumentCaptor<DnsResolver.Callback<List<InetAddress>>> resolveHostCallbackArgumentCaptor =
+                ArgumentCaptor.forClass(DnsResolver.Callback.class);
+        verify(mMockDnsResolver, times(1))
+                .query(
+                        eq(mNetwork),
+                        eq("test.local"),
+                        eq(DnsResolver.FLAG_NO_CACHE_LOOKUP),
+                        any(Executor.class),
+                        any(CancellationSignal.class),
+                        resolveHostCallbackArgumentCaptor.capture());
+        resolveHostCallbackArgumentCaptor
+                .getValue()
+                .onError(new DnsResolver.DnsException(ERROR_SYSTEM, null /* cause */));
+        mTestLooper.dispatchAll();
+
+        verify(mResolveHostCallback, times(1)).onHostResolved("test", Collections.emptyList());
+    }
+
+    @Test
+    public void stopHostResolution() throws Exception {
+        prepareTest();
+
+        mNsdPublisher.resolveHost("test", mResolveHostCallback, 10 /* listenerId */);
+        mTestLooper.dispatchAll();
+        ArgumentCaptor<CancellationSignal> cancellationSignalArgumentCaptor =
+                ArgumentCaptor.forClass(CancellationSignal.class);
+        verify(mMockDnsResolver, times(1))
+                .query(
+                        eq(mNetwork),
+                        eq("test.local"),
+                        eq(DnsResolver.FLAG_NO_CACHE_LOOKUP),
+                        any(Executor.class),
+                        cancellationSignalArgumentCaptor.capture(),
+                        any(DnsResolver.Callback.class));
+
+        mNsdPublisher.stopHostResolution(10 /* listenerId */);
+        mTestLooper.dispatchAll();
+
+        assertThat(cancellationSignalArgumentCaptor.getValue().isCanceled()).isTrue();
+    }
+
+    @Test
+    public void reset_unregisterAll() {
+        prepareTest();
         ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
                 ArgumentCaptor.forClass(NsdServiceInfo.class);
         ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
@@ -654,7 +722,7 @@
                 "_test._tcp",
                 List.of("_subtype1", "_subtype2"),
                 12345,
-                List.of(txt1, txt2),
+                List.of(TEST_TXT_ENTRY_1, TEST_TXT_ENTRY_2),
                 mRegistrationReceiver,
                 16 /* listenerId */);
         mTestLooper.dispatchAll();
@@ -728,19 +796,6 @@
         verify(spyNsdPublisher, times(1)).reset();
     }
 
-    private static DnsTxtAttribute makeTxtAttribute(String name, List<Integer> value) {
-        DnsTxtAttribute txtAttribute = new DnsTxtAttribute();
-
-        txtAttribute.name = name;
-        txtAttribute.value = new byte[value.size()];
-
-        for (int i = 0; i < value.size(); ++i) {
-            txtAttribute.value[i] = value.get(i).byteValue();
-        }
-
-        return txtAttribute;
-    }
-
     private static List<InetAddress> makeAddresses(String... addressStrings) {
         List<InetAddress> addresses = new ArrayList<>();
 
@@ -750,36 +805,13 @@
         return addresses;
     }
 
-    private static class TxtMatcher implements ArgumentMatcher<List<DnsTxtAttribute>> {
-        private final List<DnsTxtAttribute> mAttributes;
-
-        TxtMatcher(List<DnsTxtAttribute> attributes) {
-            mAttributes = attributes;
-        }
-
-        @Override
-        public boolean matches(List<DnsTxtAttribute> argument) {
-            if (argument.size() != mAttributes.size()) {
-                return false;
-            }
-            for (int i = 0; i < argument.size(); ++i) {
-                if (!Objects.equals(argument.get(i).name, mAttributes.get(i).name)) {
-                    return false;
-                }
-                if (!Arrays.equals(argument.get(i).value, mAttributes.get(i).value)) {
-                    return false;
-                }
-            }
-            return true;
-        }
-    }
-
     // @Before and @Test run in different threads. NsdPublisher requires the jobs are run on the
     // thread looper, so TestLooper needs to be created inside each test case to install the
     // correct looper.
     private void prepareTest() {
         mTestLooper = new TestLooper();
         Handler handler = new Handler(mTestLooper.getLooper());
-        mNsdPublisher = new NsdPublisher(mMockNsdManager, handler);
+        mNsdPublisher = new NsdPublisher(mMockNsdManager, mMockDnsResolver, handler);
+        mNsdPublisher.setNetworkForHostResolution(mNetwork);
     }
 }
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
index 8f60783..2f58943 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -78,6 +78,7 @@
 import com.android.connectivity.resources.R;
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
 import com.android.server.connectivity.ConnectivityResources;
+import com.android.server.thread.openthread.DnsTxtAttribute;
 import com.android.server.thread.openthread.MeshcopTxtAttributes;
 import com.android.server.thread.openthread.testing.FakeOtDaemon;
 
@@ -94,6 +95,7 @@
 import org.mockito.MockitoAnnotations;
 import org.mockito.MockitoSession;
 
+import java.nio.charset.StandardCharsets;
 import java.time.Clock;
 import java.time.DateTimeException;
 import java.util.concurrent.CompletableFuture;
@@ -145,6 +147,7 @@
     private static final byte[] TEST_VENDOR_OUI_BYTES = new byte[] {(byte) 0xAC, (byte) 0xDE, 0x48};
     private static final String TEST_VENDOR_NAME = "test vendor";
     private static final String TEST_MODEL_NAME = "test model";
+    private static final boolean TEST_VGH_VALUE = false;
 
     @Mock private ConnectivityManager mMockConnectivityManager;
     @Mock private NetworkAgent mMockNetworkAgent;
@@ -197,6 +200,8 @@
                 .thenReturn(TEST_VENDOR_OUI);
         when(mResources.getString(eq(R.string.config_thread_model_name)))
                 .thenReturn(TEST_MODEL_NAME);
+        when(mResources.getBoolean(eq(R.bool.config_thread_managed_by_google_home)))
+                .thenReturn(TEST_VGH_VALUE);
 
         final AtomicFile storageFile = new AtomicFile(tempFolder.newFile("thread_settings.xml"));
         mPersistentSettings = new ThreadPersistentSettings(storageFile, mConnectivityResources);
@@ -232,13 +237,15 @@
     }
 
     @Test
-    public void initialize_vendorAndModelNameInResourcesAreSetToOtDaemon() throws Exception {
+    public void initialize_resourceOverlayValuesAreSetToOtDaemon() throws Exception {
         when(mResources.getString(eq(R.string.config_thread_vendor_name)))
                 .thenReturn(TEST_VENDOR_NAME);
         when(mResources.getString(eq(R.string.config_thread_vendor_oui)))
                 .thenReturn(TEST_VENDOR_OUI);
         when(mResources.getString(eq(R.string.config_thread_model_name)))
                 .thenReturn(TEST_MODEL_NAME);
+        when(mResources.getBoolean(eq(R.bool.config_thread_managed_by_google_home)))
+                .thenReturn(true);
 
         mService.initialize();
         mTestLooper.dispatchAll();
@@ -247,6 +254,20 @@
         assertThat(meshcopTxts.vendorName).isEqualTo(TEST_VENDOR_NAME);
         assertThat(meshcopTxts.vendorOui).isEqualTo(TEST_VENDOR_OUI_BYTES);
         assertThat(meshcopTxts.modelName).isEqualTo(TEST_MODEL_NAME);
+        assertThat(meshcopTxts.nonStandardTxtEntries)
+                .containsExactly(new DnsTxtAttribute("vgh", "1".getBytes(StandardCharsets.UTF_8)));
+    }
+
+    @Test
+    public void getMeshcopTxtAttributes_managedByGoogleIsFalse_vghIsZero() {
+        when(mResources.getBoolean(eq(R.bool.config_thread_managed_by_google_home)))
+                .thenReturn(false);
+
+        MeshcopTxtAttributes meshcopTxts =
+                ThreadNetworkControllerService.getMeshcopTxtAttributes(mResources);
+
+        assertThat(meshcopTxts.nonStandardTxtEntries)
+                .containsExactly(new DnsTxtAttribute("vgh", "0".getBytes(StandardCharsets.UTF_8)));
     }
 
     @Test