Merge changes Ic6ff7a3d,Iff9b9792

* changes:
  Refactor code and improve tests for VPN filtering
  Remove LOCKDOWN from FirewallChain IntDef
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index adcc236..19d0d5f 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -179,9 +179,9 @@
     certificate: "networkstack",
     manifest: "AndroidManifest.xml",
     use_embedded_native_libs: true,
-    // The permission configuration *must* be included to ensure security of the device
+    // The network stack *must* be included to ensure security of the device
     required: [
-        "NetworkPermissionConfig",
+        "NetworkStack",
         "privapp_allowlist_com.android.tethering",
     ],
     apex_available: ["com.android.tethering"],
@@ -199,9 +199,9 @@
     certificate: "networkstack",
     manifest: "AndroidManifest.xml",
     use_embedded_native_libs: true,
-    // The permission configuration *must* be included to ensure security of the device
+    // The network stack *must* be included to ensure security of the device
     required: [
-        "NetworkPermissionConfig",
+        "NetworkStackNext",
         "privapp_allowlist_com.android.tethering",
     ],
     apex_available: ["com.android.tethering"],
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index 92be84d..819936d 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -26,9 +26,8 @@
 import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
 import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
 import static android.net.TetheringManager.TETHERING_ETHERNET;
-import static android.net.TetheringTester.RemoteResponder;
-import static android.net.TetheringTester.isIcmpv6Type;
-import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.net.TetheringTester.isExpectedIcmpv6Packet;
+import static android.net.TetheringTester.isExpectedUdpPacket;
 import static android.system.OsConstants.IPPROTO_IP;
 import static android.system.OsConstants.IPPROTO_IPV6;
 import static android.system.OsConstants.IPPROTO_UDP;
@@ -84,11 +83,7 @@
 import com.android.net.module.util.bpf.Tether4Value;
 import com.android.net.module.util.bpf.TetherStatsKey;
 import com.android.net.module.util.bpf.TetherStatsValue;
-import com.android.net.module.util.structs.EthernetHeader;
-import com.android.net.module.util.structs.Icmpv6Header;
-import com.android.net.module.util.structs.Ipv4Header;
 import com.android.net.module.util.structs.Ipv6Header;
-import com.android.net.module.util.structs.UdpHeader;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
@@ -146,6 +141,7 @@
     // Per TX UDP packet size: ethhdr (14) + iphdr (20) + udphdr (8) + payload (2) = 44 bytes.
     private static final int TX_UDP_PACKET_SIZE = 44;
     private static final int TX_UDP_PACKET_COUNT = 123;
+    private static final long WAIT_RA_TIMEOUT_MS = 2000;
 
     private static final LinkAddress TEST_IP4_ADDR = new LinkAddress("10.0.0.1/8");
     private static final LinkAddress TEST_IP6_ADDR = new LinkAddress("2001:db8:1::101/64");
@@ -329,27 +325,13 @@
 
     }
 
-    private static boolean isRouterAdvertisement(byte[] pkt) {
-        if (pkt == null) return false;
-
-        ByteBuffer buf = ByteBuffer.wrap(pkt);
-
-        final EthernetHeader ethHdr = Struct.parse(EthernetHeader.class, buf);
-        if (ethHdr.etherType != ETHER_TYPE_IPV6) return false;
-
-        final Ipv6Header ipv6Hdr = Struct.parse(Ipv6Header.class, buf);
-        if (ipv6Hdr.nextHeader != (byte) IPPROTO_ICMPV6) return false;
-
-        final Icmpv6Header icmpv6Hdr = Struct.parse(Icmpv6Header.class, buf);
-        return icmpv6Hdr.type == (short) ICMPV6_ROUTER_ADVERTISEMENT;
-    }
-
-    private static void expectRouterAdvertisement(TapPacketReader reader, String iface,
+    private static void waitForRouterAdvertisement(TapPacketReader reader, String iface,
             long timeoutMs) {
         final long deadline = SystemClock.uptimeMillis() + timeoutMs;
         do {
             byte[] pkt = reader.popPacket(timeoutMs);
-            if (isRouterAdvertisement(pkt)) return;
+            if (isExpectedIcmpv6Packet(pkt, true /* hasEth */, ICMPV6_ROUTER_ADVERTISEMENT)) return;
+
             timeoutMs = deadline - SystemClock.uptimeMillis();
         } while (timeoutMs > 0);
         fail("Did not receive router advertisement on " + iface + " after "
@@ -401,7 +383,7 @@
         // before the reader is started.
         mDownstreamReader = makePacketReader(mDownstreamIface);
 
-        expectRouterAdvertisement(mDownstreamReader, iface, 2000 /* timeoutMs */);
+        waitForRouterAdvertisement(mDownstreamReader, iface, WAIT_RA_TIMEOUT_MS);
         expectLocalOnlyAddresses(iface);
     }
 
@@ -808,61 +790,28 @@
 
     @Test
     public void testIcmpv6Echo() throws Exception {
-        assumeFalse(mEm.isAvailable());
-
-        // MyTetheringEventCallback currently only support await first available upstream. Tethering
-        // may select internet network as upstream if test network is not available and not be
-        // preferred yet. Create test upstream network before enable tethering.
-        mUpstreamTracker = createTestUpstream(toList(TEST_IP4_ADDR, TEST_IP6_ADDR),
-                toList(TEST_IP4_DNS, TEST_IP6_DNS));
-
-        mDownstreamIface = createTestInterface();
-        mEm.setIncludeTestInterfaces(true);
-
-        final String iface = mTetheredInterfaceRequester.getInterface();
-        assertEquals("TetheredInterfaceCallback for unexpected interface",
-                mDownstreamIface.getInterfaceName(), iface);
-
-        mTetheringEventCallback = enableEthernetTethering(mDownstreamIface.getInterfaceName(),
-                mUpstreamTracker.getNetwork());
-        assertEquals("onUpstreamChanged for unexpected network", mUpstreamTracker.getNetwork(),
-                mTetheringEventCallback.awaitUpstreamChanged());
-
-        mDownstreamReader = makePacketReader(mDownstreamIface);
-        mUpstreamReader = makePacketReader(mUpstreamTracker.getTestIface());
-
-        runPing6Test(new TetheringTester(mDownstreamReader), new RemoteResponder(mUpstreamReader));
+        runPing6Test(initTetheringTester(toList(TEST_IP4_ADDR, TEST_IP6_ADDR),
+                toList(TEST_IP4_DNS, TEST_IP6_DNS)));
     }
 
-    private void runPing6Test(TetheringTester tester, RemoteResponder remote) throws Exception {
-        // Currently tethering don't have API to tell when ipv6 tethering is available. Thus, let
-        // TetheringTester test ipv6 tethering connectivity before testing ipv6.
-        // TODO: move to a common place to avoid that every IPv6 test needs to call this function.
-        tester.waitForIpv6TetherConnectivityVerified();
-
+    private void runPing6Test(TetheringTester tester) throws Exception {
         TetheredDevice tethered = tester.createTetheredDevice(MacAddress.fromString("1:2:3:4:5:6"),
                 true /* hasIpv6 */);
         Inet6Address remoteIp6Addr = (Inet6Address) parseNumericAddress("2400:222:222::222");
         ByteBuffer request = Ipv6Utils.buildEchoRequestPacket(tethered.macAddr,
                 tethered.routerMacAddr, tethered.ipv6Addr, remoteIp6Addr);
-        tester.sendPacket(request);
-
-        final byte[] echoRequest = remote.getNextMatchedPacket((p) -> {
+        tester.verifyUpload(request, p -> {
             Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
 
-            return isIcmpv6Type(p, false /* hasEth */, ICMPV6_ECHO_REQUEST_TYPE);
+            return isExpectedIcmpv6Packet(p, false /* hasEth */, ICMPV6_ECHO_REQUEST_TYPE);
         });
-        assertNotNull("No icmpv6 echo request in upstream", echoRequest);
 
         ByteBuffer reply = Ipv6Utils.buildEchoReplyPacket(remoteIp6Addr, tethered.ipv6Addr);
-        remote.sendPacket(reply);
-
-        final byte[] echoReply = tester.getNextMatchedPacket((p) -> {
+        tester.verifyDownload(reply, p -> {
             Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
 
-            return isIcmpv6Type(p, true /* hasEth */, ICMPV6_ECHO_REPLY_TYPE);
+            return isExpectedIcmpv6Packet(p, true /* hasEth */, ICMPV6_ECHO_REPLY_TYPE);
         });
-        assertNotNull("No icmpv6 echo reply in downstream", echoReply);
     }
 
     // Test network topology:
@@ -894,28 +843,6 @@
     private static final ByteBuffer PAYLOAD3 =
             ByteBuffer.wrap(new byte[] { (byte) 0x9a, (byte) 0xbc });
 
-    private boolean isExpectedUdpPacket(@NonNull final byte[] rawPacket, boolean hasEther,
-            boolean isIpv4, @NonNull final ByteBuffer payload) {
-        final ByteBuffer buf = ByteBuffer.wrap(rawPacket);
-
-        if (hasEther) {
-            if (Struct.parse(EthernetHeader.class, buf) == null) return false;
-        }
-
-        if (isIpv4) {
-            if (Struct.parse(Ipv4Header.class, buf) == null) return false;
-        } else {
-            if (Struct.parse(Ipv6Header.class, buf) == null) return false;
-        }
-
-        if (Struct.parse(UdpHeader.class, buf) == null) return false;
-
-        if (buf.remaining() != payload.limit()) return false;
-
-        return Arrays.equals(Arrays.copyOfRange(buf.array(), buf.position(), buf.limit()),
-                payload.array());
-    }
-
     @NonNull
     private ByteBuffer buildUdpPacket(
             @Nullable final MacAddress srcMac, @Nullable final MacAddress dstMac,
@@ -976,10 +903,15 @@
                 dstPort, payload);
     }
 
-    // TODO: remove this verification once upstream connected notification race is fixed.
-    // See #runUdp4Test.
-    private boolean isIpv4TetherConnectivityVerified(TetheringTester tester,
-            RemoteResponder remote, TetheredDevice tethered) throws Exception {
+    // TODO: remove ipv4 verification (is4To6 = false) once upstream connected notification race is
+    // fixed. See #runUdp4Test.
+    //
+    // This function sends a probe packet to downstream interface and exam the result from upstream
+    // interface to make sure ipv4 tethering is ready. Return the entire packet which received from
+    // upstream interface.
+    @NonNull
+    private byte[] probeV4TetheringConnectivity(TetheringTester tester, TetheredDevice tethered,
+            boolean is4To6) throws Exception {
         final ByteBuffer probePacket = buildUdpPacket(tethered.macAddr,
                 tethered.routerMacAddr, tethered.ipv4Addr /* srcIp */,
                 REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */, REMOTE_PORT /* dstPort */,
@@ -987,19 +919,22 @@
 
         // Send a UDP packet from client and check the packet can be found on upstream interface.
         for (int i = 0; i < TETHER_REACHABILITY_ATTEMPTS; i++) {
-            tester.sendPacket(probePacket);
-            byte[] expectedPacket = remote.getNextMatchedPacket(p -> {
+            byte[] expectedPacket = tester.testUpload(probePacket, p -> {
                 Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
-                return isExpectedUdpPacket(p, false /* hasEther */, true /* isIpv4 */,
+                // If is4To6 is true, the ipv4 probe packet would be translated to ipv6 by Clat and
+                // would see this translated ipv6 packet in upstream interface.
+                return isExpectedUdpPacket(p, false /* hasEther */, !is4To6 /* isIpv4 */,
                         TEST_REACHABILITY_PAYLOAD);
             });
-            if (expectedPacket != null) return true;
+            if (expectedPacket != null) return expectedPacket;
         }
-        return false;
+
+        fail("Can't verify " + (is4To6 ? "ipv4 to ipv6" : "ipv4") + " tethering connectivity after "
+                + TETHER_REACHABILITY_ATTEMPTS + " attempts");
+        return null;
     }
 
-    private void runUdp4Test(TetheringTester tester, RemoteResponder remote, boolean usingBpf)
-            throws Exception {
+    private void runUdp4Test(TetheringTester tester, boolean usingBpf) throws Exception {
         final TetheredDevice tethered = tester.createTetheredDevice(MacAddress.fromString(
                 "1:2:3:4:5:6"), false /* hasIpv6 */);
 
@@ -1009,14 +944,14 @@
         // For short term plan, consider using IPv6 RA to get MAC address because the prefix comes
         // from upstream. That can guarantee that the routing is ready. Long term plan is that
         // refactors upstream connected notification from async to sync.
-        assertTrue(isIpv4TetherConnectivityVerified(tester, remote, tethered));
+        probeV4TetheringConnectivity(tester, tethered, false /* is4To6 */);
 
         // Send a UDP packet in original direction.
         final ByteBuffer originalPacket = buildUdpPacket(tethered.macAddr,
                 tethered.routerMacAddr, tethered.ipv4Addr /* srcIp */,
                 REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */, REMOTE_PORT /* dstPort */,
                 PAYLOAD /* payload */);
-        tester.verifyUpload(remote, originalPacket, p -> {
+        tester.verifyUpload(originalPacket, p -> {
             Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
             return isExpectedUdpPacket(p, false /* hasEther */, true /* isIpv4 */, PAYLOAD);
         });
@@ -1026,7 +961,7 @@
         final ByteBuffer replyPacket = buildUdpPacket(REMOTE_IP4_ADDR /* srcIp */,
                 publicIp4Addr /* dstIp */, REMOTE_PORT /* srcPort */, LOCAL_PORT /* dstPort */,
                 PAYLOAD2 /* payload */);
-        remote.verifyDownload(tester, replyPacket, p -> {
+        tester.verifyDownload(replyPacket, p -> {
             Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
             return isExpectedUdpPacket(p, true /* hasEther */, true /* isIpv4 */, PAYLOAD2);
         });
@@ -1045,7 +980,7 @@
                     tethered.routerMacAddr, tethered.ipv4Addr /* srcIp */,
                     REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */,
                     REMOTE_PORT /* dstPort */, PAYLOAD3 /* payload */);
-            tester.verifyUpload(remote, originalPacket2, p -> {
+            tester.verifyUpload(originalPacket2, p -> {
                 Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
                 return isExpectedUdpPacket(p, false /* hasEther */, true /* isIpv4 */, PAYLOAD3);
             });
@@ -1081,7 +1016,7 @@
 
             // Send packets on original direction.
             for (int i = 0; i < TX_UDP_PACKET_COUNT; i++) {
-                tester.verifyUpload(remote, originalPacket, p -> {
+                tester.verifyUpload(originalPacket, p -> {
                     Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
                     return isExpectedUdpPacket(p, false /* hasEther */, true /* isIpv4 */, PAYLOAD);
                 });
@@ -1089,7 +1024,7 @@
 
             // Send packets on reply direction.
             for (int i = 0; i < RX_UDP_PACKET_COUNT; i++) {
-                remote.verifyDownload(tester, replyPacket, p -> {
+                tester.verifyDownload(replyPacket, p -> {
                     Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
                     return isExpectedUdpPacket(p, true /* hasEther */, true /* isIpv4 */, PAYLOAD2);
                 });
@@ -1116,8 +1051,8 @@
         }
     }
 
-    void initializeTethering(List<LinkAddress> upstreamAddresses, List<InetAddress> upstreamDnses)
-            throws Exception {
+    private TetheringTester initTetheringTester(List<LinkAddress> upstreamAddresses,
+            List<InetAddress> upstreamDnses) throws Exception {
         assumeFalse(mEm.isAvailable());
 
         // MyTetheringEventCallback currently only support await first available upstream. Tethering
@@ -1128,9 +1063,9 @@
         mDownstreamIface = createTestInterface();
         mEm.setIncludeTestInterfaces(true);
 
-        final String iface = mTetheredInterfaceRequester.getInterface();
+        // Make sure EtherentTracker use "mDownstreamIface" as server mode interface.
         assertEquals("TetheredInterfaceCallback for unexpected interface",
-                mDownstreamIface.getInterfaceName(), iface);
+                mDownstreamIface.getInterfaceName(), mTetheredInterfaceRequester.getInterface());
 
         mTetheringEventCallback = enableEthernetTethering(mDownstreamIface.getInterfaceName(),
                 mUpstreamTracker.getNetwork());
@@ -1139,13 +1074,22 @@
 
         mDownstreamReader = makePacketReader(mDownstreamIface);
         mUpstreamReader = makePacketReader(mUpstreamTracker.getTestIface());
+
+        final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
+        // Currently tethering don't have API to tell when ipv6 tethering is available. Thus, make
+        // sure tethering already have ipv6 connectivity before testing.
+        if (cm.getLinkProperties(mUpstreamTracker.getNetwork()).hasGlobalIpv6Address()) {
+            waitForRouterAdvertisement(mDownstreamReader, mDownstreamIface.getInterfaceName(),
+                    WAIT_RA_TIMEOUT_MS);
+        }
+
+        return new TetheringTester(mDownstreamReader, mUpstreamReader);
     }
 
     @Test
     @IgnoreAfter(Build.VERSION_CODES.R)
     public void testTetherUdpV4UpToR() throws Exception {
-        initializeTethering(toList(TEST_IP4_ADDR), toList(TEST_IP4_DNS));
-        runUdp4Test(new TetheringTester(mDownstreamReader), new RemoteResponder(mUpstreamReader),
+        runUdp4Test(initTetheringTester(toList(TEST_IP4_ADDR), toList(TEST_IP4_DNS)),
                 false /* usingBpf */);
     }
 
@@ -1180,15 +1124,13 @@
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.R)
     public void testTetherUdpV4AfterR() throws Exception {
-        initializeTethering(toList(TEST_IP4_ADDR), toList(TEST_IP4_DNS));
         final String kernelVersion = VintfRuntimeInfo.getKernelVersion();
         boolean usingBpf = isUdpOffloadSupportedByKernel(kernelVersion);
         if (!usingBpf) {
             Log.i(TAG, "testTetherUdpV4AfterR will skip BPF offload test for kernel "
                     + kernelVersion);
         }
-        runUdp4Test(new TetheringTester(mDownstreamReader), new RemoteResponder(mUpstreamReader),
-                usingBpf);
+        runUdp4Test(initTetheringTester(toList(TEST_IP4_ADDR), toList(TEST_IP4_DNS)), usingBpf);
     }
 
     @Nullable
@@ -1247,33 +1189,16 @@
         return null;
     }
 
-    @Nullable
-    private Inet6Address getClatIpv6Address(TetheringTester tester,
-            RemoteResponder remote, TetheredDevice tethered) throws Exception {
-        final ByteBuffer probePacket = buildUdpPacket(tethered.macAddr,
-                tethered.routerMacAddr, tethered.ipv4Addr /* srcIp */,
-                REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */, REMOTE_PORT /* dstPort */,
-                TEST_REACHABILITY_PAYLOAD);
-
+    @NonNull
+    private Inet6Address getClatIpv6Address(TetheringTester tester, TetheredDevice tethered)
+            throws Exception {
         // Send an IPv4 UDP packet from client and check that a CLAT translated IPv6 UDP packet can
         // be found on upstream interface. Get CLAT IPv6 address from the CLAT translated IPv6 UDP
         // packet.
-        byte[] expectedPacket = null;
-        for (int i = 0; i < TETHER_REACHABILITY_ATTEMPTS; i++) {
-            tester.sendPacket(probePacket);
-            expectedPacket = remote.getNextMatchedPacket(p -> {
-                Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
-                return isExpectedUdpPacket(p, false /* hasEther */, false /* isIpv4 */,
-                        TEST_REACHABILITY_PAYLOAD);
-            });
-            if (expectedPacket != null) break;
-        }
-        if (expectedPacket == null) return null;
+        byte[] expectedPacket = probeV4TetheringConnectivity(tester, tethered, true /* is4To6 */);
 
         // Above has guaranteed that the found packet is an IPv6 packet without ether header.
-        final Ipv6Header ipv6Header = Struct.parse(Ipv6Header.class,
-                ByteBuffer.wrap(expectedPacket));
-        return ipv6Header.srcIp;
+        return Struct.parse(Ipv6Header.class, ByteBuffer.wrap(expectedPacket)).srcIp;
     }
 
     // Test network topology:
@@ -1290,19 +1215,12 @@
     // sending out an IPv4 packet and extracting the source address from CLAT translated IPv6
     // packet.
     //
-    private void runClatUdpTest(TetheringTester tester, RemoteResponder remote)
-            throws Exception {
-        // Currently tethering don't have API to tell when ipv6 tethering is available. Thus, let
-        // TetheringTester test ipv6 tethering connectivity before testing ipv6.
-        // TODO: move to a common place to avoid that every IPv6 test needs to call this function.
-        tester.waitForIpv6TetherConnectivityVerified();
-
+    private void runClatUdpTest(TetheringTester tester) throws Exception {
         final TetheredDevice tethered = tester.createTetheredDevice(MacAddress.fromString(
                 "1:2:3:4:5:6"), true /* hasIpv6 */);
 
         // Get CLAT IPv6 address.
-        final Inet6Address clatAddr6 = getClatIpv6Address(tester, remote, tethered);
-        assertNotNull(clatAddr6);
+        final Inet6Address clatAddr6 = getClatIpv6Address(tester, tethered);
 
         // Send an IPv4 UDP packet in original direction.
         // IPv4 packet -- CLAT translation --> IPv6 packet
@@ -1310,7 +1228,7 @@
                 tethered.routerMacAddr, tethered.ipv4Addr /* srcIp */,
                 REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */, REMOTE_PORT /* dstPort */,
                 PAYLOAD /* payload */);
-        tester.verifyUpload(remote, originalPacket, p -> {
+        tester.verifyUpload(originalPacket, p -> {
             Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
             return isExpectedUdpPacket(p, false /* hasEther */, false /* isIpv4 */, PAYLOAD);
         });
@@ -1320,7 +1238,7 @@
         final ByteBuffer replyPacket = buildUdpPacket(REMOTE_NAT64_ADDR /* srcIp */,
                 clatAddr6 /* dstIp */, REMOTE_PORT /* srcPort */, LOCAL_PORT /* dstPort */,
                 PAYLOAD2 /* payload */);
-        remote.verifyDownload(tester, replyPacket, p -> {
+        tester.verifyDownload(replyPacket, p -> {
             Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
             return isExpectedUdpPacket(p, true /* hasEther */, true /* isIpv4 */, PAYLOAD2);
         });
@@ -1332,9 +1250,7 @@
     @IgnoreUpTo(Build.VERSION_CODES.R)
     public void testTetherClatUdp() throws Exception {
         // CLAT only starts on IPv6 only network.
-        initializeTethering(toList(TEST_IP6_ADDR), toList(TEST_IP6_DNS));
-        runClatUdpTest(new TetheringTester(mDownstreamReader),
-                new RemoteResponder(mUpstreamReader));
+        runClatUdpTest(initTetheringTester(toList(TEST_IP6_ADDR), toList(TEST_IP6_DNS)));
     }
 
     private <T> List<T> toList(T... array) {
diff --git a/Tethering/tests/integration/src/android/net/TetheringTester.java b/Tethering/tests/integration/src/android/net/TetheringTester.java
index 458680a..4d90d39 100644
--- a/Tethering/tests/integration/src/android/net/TetheringTester.java
+++ b/Tethering/tests/integration/src/android/net/TetheringTester.java
@@ -18,11 +18,13 @@
 
 import static android.net.InetAddresses.parseNumericAddress;
 import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.system.OsConstants.IPPROTO_UDP;
 
 import static com.android.net.module.util.NetworkStackConstants.ARP_REPLY;
 import static com.android.net.module.util.NetworkStackConstants.ARP_REQUEST;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_BROADCAST;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV4;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_PIO;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
@@ -42,17 +44,20 @@
 import android.util.ArrayMap;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.net.module.util.Ipv6Utils;
 import com.android.net.module.util.Struct;
 import com.android.net.module.util.structs.EthernetHeader;
 import com.android.net.module.util.structs.Icmpv6Header;
+import com.android.net.module.util.structs.Ipv4Header;
 import com.android.net.module.util.structs.Ipv6Header;
 import com.android.net.module.util.structs.LlaOption;
 import com.android.net.module.util.structs.NsHeader;
 import com.android.net.module.util.structs.PrefixInformationOption;
 import com.android.net.module.util.structs.RaHeader;
+import com.android.net.module.util.structs.UdpHeader;
 import com.android.networkstack.arp.ArpPacket;
 import com.android.testutils.TapPacketReader;
 
@@ -61,6 +66,7 @@
 import java.net.InetAddress;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Random;
 import java.util.concurrent.TimeoutException;
@@ -88,11 +94,17 @@
 
     private final ArrayMap<MacAddress, TetheredDevice> mTetheredDevices;
     private final TapPacketReader mDownstreamReader;
+    private final TapPacketReader mUpstreamReader;
 
     public TetheringTester(TapPacketReader downstream) {
+        this(downstream, null);
+    }
+
+    public TetheringTester(TapPacketReader downstream, TapPacketReader upstream) {
         if (downstream == null) fail("Downstream reader could not be NULL");
 
         mDownstreamReader = downstream;
+        mUpstreamReader = upstream;
         mTetheredDevices = new ArrayMap<>();
     }
 
@@ -170,7 +182,7 @@
     }
 
     private DhcpPacket getNextDhcpPacket() throws Exception {
-        final byte[] packet = getNextMatchedPacket((p) -> {
+        final byte[] packet = getDownloadPacket((p) -> {
             // Test whether this is DHCP packet.
             try {
                 DhcpPacket.decodeFullPacket(p, p.length, DhcpPacket.ENCAP_L2);
@@ -213,7 +225,7 @@
                     tethered.ipv4Addr.getAddress() /* sender IP */,
                     (short) ARP_REPLY);
             try {
-                sendPacket(arpReply);
+                sendUploadPacket(arpReply);
             } catch (Exception e) {
                 fail("Failed to reply ARP for " + tethered.ipv4Addr);
             }
@@ -227,9 +239,9 @@
                 tetherMac.toByteArray() /* srcMac */, routerIp.getAddress() /* target IP */,
                 new byte[ETHER_ADDR_LEN] /* target HW address */,
                 tetherIp.getAddress() /* sender IP */, (short) ARP_REQUEST);
-        sendPacket(arpProbe);
+        sendUploadPacket(arpProbe);
 
-        final byte[] packet = getNextMatchedPacket((p) -> {
+        final byte[] packet = getDownloadPacket((p) -> {
             final ArpPacket arpPacket = parseArpPacket(p);
             if (arpPacket == null || arpPacket.opCode != ARP_REPLY) return false;
             return arpPacket.targetIp.equals(tetherIp);
@@ -245,25 +257,11 @@
         return null;
     }
 
-    public void waitForIpv6TetherConnectivityVerified() throws Exception {
-        Log.d(TAG, "Waiting RA multicast");
-
-        // Wait for RA multicast message from router to confirm that the IPv6 tethering
-        // connectivity is ready. We don't extract the router mac address from RA because
-        // we get the router mac address from IPv4 ARP packet. See #getRouterMacAddressFromArp.
-        for (int i = 0; i < READ_RA_ATTEMPTS; i++) {
-            final byte[] raPacket = getNextMatchedPacket((p) -> {
-                return isIcmpv6Type(p, true /* hasEth */, ICMPV6_ROUTER_ADVERTISEMENT);
-            });
-            if (raPacket != null) return;
-        }
-
-        fail("Could not get RA multicast packet after " + READ_RA_ATTEMPTS + " attempts");
-    }
-
     private List<PrefixInformationOption> getRaPrefixOptions(byte[] packet) {
         ByteBuffer buf = ByteBuffer.wrap(packet);
-        if (!isIcmpv6Type(buf, true /* hasEth */, ICMPV6_ROUTER_ADVERTISEMENT)) return null;
+        if (!isExpectedIcmpv6Packet(buf, true /* hasEth */, ICMPV6_ROUTER_ADVERTISEMENT)) {
+            fail("Parsing RA packet fail");
+        }
 
         Struct.parse(RaHeader.class, buf);
         final ArrayList<PrefixInformationOption> pioList = new ArrayList<>();
@@ -290,13 +288,10 @@
     private Inet6Address runSlaac(MacAddress srcMac, MacAddress dstMac) throws Exception {
         sendRsPacket(srcMac, dstMac);
 
-        final byte[] raPacket = getNextMatchedPacket((p) -> {
-            return isIcmpv6Type(p, true /* hasEth */, ICMPV6_ROUTER_ADVERTISEMENT);
-        });
+        final byte[] raPacket = verifyPacketNotNull("Receive RA fail", getDownloadPacket(p -> {
+            return isExpectedIcmpv6Packet(p, true /* hasEth */, ICMPV6_ROUTER_ADVERTISEMENT);
+        }));
 
-        if (raPacket == null) {
-            fail("Could not get ra for prefix options");
-        }
         final List<PrefixInformationOption> options = getRaPrefixOptions(raPacket);
 
         for (PrefixInformationOption pio : options) {
@@ -312,7 +307,7 @@
             }
         }
 
-        fail("Could not get ipv6 address");
+        fail("No available ipv6 prefix");
         return null;
     }
 
@@ -322,7 +317,7 @@
         ByteBuffer rs = Ipv6Utils.buildRsPacket(srcMac, dstMac, (Inet6Address) LINK_LOCAL,
                 IPV6_ADDR_ALL_NODES_MULTICAST, slla);
 
-        sendPacket(rs);
+        sendUploadPacket(rs);
     }
 
     private void maybeReplyNa(byte[] packet) {
@@ -347,7 +342,7 @@
             ByteBuffer ns = Ipv6Utils.buildNaPacket(tethered.macAddr, tethered.routerMacAddr,
                     nsHdr.target, ipv6Hdr.srcIp, flags, nsHdr.target, tlla);
             try {
-                sendPacket(ns);
+                sendUploadPacket(ns);
             } catch (Exception e) {
                 fail("Failed to reply NA for " + tethered.ipv6Addr);
             }
@@ -356,23 +351,18 @@
         }
     }
 
-    public static boolean isIcmpv6Type(byte[] packet, boolean hasEth, int type) {
+    public static boolean isExpectedIcmpv6Packet(byte[] packet, boolean hasEth, int type) {
         final ByteBuffer buf = ByteBuffer.wrap(packet);
-        return isIcmpv6Type(buf, hasEth, type);
+        return isExpectedIcmpv6Packet(buf, hasEth, type);
     }
 
-    private static boolean isIcmpv6Type(ByteBuffer buf, boolean hasEth, int type) {
+    private static boolean isExpectedIcmpv6Packet(ByteBuffer buf, boolean hasEth, int type) {
         try {
-            if (hasEth) {
-                final EthernetHeader ethHdr = Struct.parse(EthernetHeader.class, buf);
-                if (ethHdr.etherType != ETHER_TYPE_IPV6) return false;
-            }
+            if (hasEth && !hasExpectedEtherHeader(buf, false /* isIpv4 */)) return false;
 
-            final Ipv6Header ipv6Hdr = Struct.parse(Ipv6Header.class, buf);
-            if (ipv6Hdr.nextHeader != (byte) IPPROTO_ICMPV6) return false;
+            if (!hasExpectedIpHeader(buf, false /* isIpv4 */, IPPROTO_ICMPV6)) return false;
 
-            final Icmpv6Header icmpv6Hdr = Struct.parse(Icmpv6Header.class, buf);
-            return icmpv6Hdr.type == (short) type;
+            return Struct.parse(Icmpv6Header.class, buf).type == (short) type;
         } catch (Exception e) {
             // Parsing packet fail means it is not icmpv6 packet.
         }
@@ -380,11 +370,53 @@
         return false;
     }
 
-    public void sendPacket(ByteBuffer packet) throws Exception {
+    private static boolean hasExpectedEtherHeader(@NonNull final ByteBuffer buf, boolean isIpv4)
+            throws Exception {
+        final int expected = isIpv4 ? ETHER_TYPE_IPV4 : ETHER_TYPE_IPV6;
+
+        return Struct.parse(EthernetHeader.class, buf).etherType == expected;
+    }
+
+    private static boolean hasExpectedIpHeader(@NonNull final ByteBuffer buf, boolean isIpv4,
+            int ipProto) throws Exception {
+        if (isIpv4) {
+            return Struct.parse(Ipv4Header.class, buf).protocol == (byte) ipProto;
+        } else {
+            return Struct.parse(Ipv6Header.class, buf).nextHeader == (byte) ipProto;
+        }
+    }
+
+    public static boolean isExpectedUdpPacket(@NonNull final byte[] rawPacket, boolean hasEth,
+            boolean isIpv4, @NonNull final ByteBuffer payload) {
+        final ByteBuffer buf = ByteBuffer.wrap(rawPacket);
+        try {
+            if (hasEth && !hasExpectedEtherHeader(buf, isIpv4)) return false;
+
+            if (!hasExpectedIpHeader(buf, isIpv4, IPPROTO_UDP)) return false;
+
+            if (Struct.parse(UdpHeader.class, buf) == null) return false;
+        } catch (Exception e) {
+            // Parsing packet fail means it is not udp packet.
+            return false;
+        }
+
+        if (buf.remaining() != payload.limit()) return false;
+
+        return Arrays.equals(Arrays.copyOfRange(buf.array(), buf.position(), buf.limit()),
+                payload.array());
+    }
+
+    private void sendUploadPacket(ByteBuffer packet) throws Exception {
         mDownstreamReader.sendResponse(packet);
     }
 
-    public byte[] getNextMatchedPacket(Predicate<byte[]> filter) {
+    private void sendDownloadPacket(ByteBuffer packet) throws Exception {
+        assertNotNull("Can't deal with upstream interface in local only mode", mUpstreamReader);
+
+        mUpstreamReader.sendResponse(packet);
+    }
+
+    private byte[] getDownloadPacket(Predicate<byte[]> filter) {
         byte[] packet;
         while ((packet = mDownstreamReader.poll(PACKET_READ_TIMEOUT_MS)) != null) {
             if (filter.test(packet)) return packet;
@@ -396,30 +428,34 @@
         return null;
     }
 
-    public void verifyUpload(final RemoteResponder dst, final ByteBuffer packet,
-            final Predicate<byte[]> filter) throws Exception {
-        sendPacket(packet);
-        assertNotNull("Upload fail", dst.getNextMatchedPacket(filter));
+    private byte[] getUploadPacket(Predicate<byte[]> filter) {
+        assertNotNull("Can't deal with upstream interface in local only mode", mUpstreamReader);
+
+        return mUpstreamReader.poll(PACKET_READ_TIMEOUT_MS, filter);
     }
 
-    public static class RemoteResponder {
-        final TapPacketReader mUpstreamReader;
-        public RemoteResponder(TapPacketReader reader) {
-            mUpstreamReader = reader;
-        }
+    private @NonNull byte[] verifyPacketNotNull(String message, @Nullable byte[] packet) {
+        assertNotNull(message, packet);
 
-        public void sendPacket(ByteBuffer packet) throws Exception {
-            mUpstreamReader.sendResponse(packet);
-        }
+        return packet;
+    }
 
-        public byte[] getNextMatchedPacket(Predicate<byte[]> filter) throws Exception {
-            return mUpstreamReader.poll(PACKET_READ_TIMEOUT_MS, filter);
-        }
+    public byte[] testUpload(final ByteBuffer packet, final Predicate<byte[]> filter)
+            throws Exception {
+        sendUploadPacket(packet);
 
-        public void verifyDownload(final TetheringTester dst, final ByteBuffer packet,
-                final Predicate<byte[]> filter) throws Exception {
-            sendPacket(packet);
-            assertNotNull("Download fail", dst.getNextMatchedPacket(filter));
-        }
+        return getUploadPacket(filter);
+    }
+
+    public byte[] verifyUpload(final ByteBuffer packet, final Predicate<byte[]> filter)
+            throws Exception {
+        return verifyPacketNotNull("Upload fail", testUpload(packet, filter));
+    }
+
+    public byte[] verifyDownload(final ByteBuffer packet, final Predicate<byte[]> filter)
+            throws Exception {
+        sendDownloadPacket(packet);
+
+        return verifyPacketNotNull("Download fail", getDownloadPacket(filter));
     }
 }
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
index 23af3e3..b1144b4 100644
--- a/bpf_progs/Android.bp
+++ b/bpf_progs/Android.bp
@@ -25,8 +25,14 @@
     name: "bpf_connectivity_headers",
     vendor_available: false,
     host_supported: false,
-    header_libs: ["bpf_headers"],
-    export_header_lib_headers: ["bpf_headers"],
+    header_libs: [
+        "bpf_headers",
+        "libnetdbinder_utils_headers",  // for XtBpfProgLocations.h
+    ],
+    export_header_lib_headers: [
+        "bpf_headers",
+        "libnetdbinder_utils_headers",  // for XtBpfProgLocations.h
+    ],
     export_include_dirs: ["."],
     cflags: [
         "-Wall",
@@ -37,11 +43,8 @@
     apex_available: [
         "//apex_available:platform",
         "com.android.tethering",
-        ],
+    ],
     visibility: [
-        // TODO: remove it when NetworkStatsService is moved into the mainline module and no more
-        // calls to JNI in libservices.core.
-        "//frameworks/base/services/core/jni",
         "//packages/modules/Connectivity/netd",
         "//packages/modules/Connectivity/service",
         "//packages/modules/Connectivity/service/native/libs/libclat",
@@ -50,7 +53,6 @@
         "//packages/modules/Connectivity/tests/native",
         "//packages/modules/Connectivity/service-t/native/libs/libnetworkstats",
         "//packages/modules/Connectivity/tests/unit/jni",
-        "//system/netd/server",
         "//system/netd/tests",
     ],
 }
@@ -106,13 +108,11 @@
         "-Wall",
         "-Werror",
     ],
-    include_dirs: [
-        "frameworks/libs/net/common/netd/libnetdutils/include",
-    ],
     sub_dir: "net_shared",
 }
 
 bpf {
+    // WARNING: Android T's non-updatable netd depends on 'netd' string for xt_bpf programs it loads
     name: "netd.o",
     srcs: ["netd.c"],
     btf: true,
@@ -120,8 +120,6 @@
         "-Wall",
         "-Werror",
     ],
-    include_dirs: [
-        "frameworks/libs/net/common/netd/libnetdutils/include",
-    ],
+    // WARNING: Android T's non-updatable netd depends on 'netd_shared' string for xt_bpf programs
     sub_dir: "netd_shared",
 }
diff --git a/bpf_progs/bpf_shared.h b/bpf_progs/bpf_shared.h
index 706dd1d..4b3ba2f 100644
--- a/bpf_progs/bpf_shared.h
+++ b/bpf_progs/bpf_shared.h
@@ -21,6 +21,11 @@
 #include <linux/in.h>
 #include <linux/in6.h>
 
+#ifdef __cplusplus
+#include <string_view>
+#include "XtBpfProgLocations.h"
+#endif
+
 // This header file is shared by eBPF kernel programs (C) and netd (C++) and
 // some of the maps are also accessed directly from Java mainline module code.
 //
@@ -98,14 +103,33 @@
 static const int CONFIGURATION_MAP_SIZE = 2;
 static const int UID_OWNER_MAP_SIZE = 2000;
 
+#ifdef __cplusplus
+
 #define BPF_NETD_PATH "/sys/fs/bpf/netd_shared/"
 
 #define BPF_EGRESS_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupskb_egress_stats"
 #define BPF_INGRESS_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupskb_ingress_stats"
-#define XT_BPF_INGRESS_PROG_PATH BPF_NETD_PATH "prog_netd_skfilter_ingress_xtbpf"
-#define XT_BPF_EGRESS_PROG_PATH BPF_NETD_PATH "prog_netd_skfilter_egress_xtbpf"
-#define XT_BPF_ALLOWLIST_PROG_PATH BPF_NETD_PATH "prog_netd_skfilter_allowlist_xtbpf"
-#define XT_BPF_DENYLIST_PROG_PATH BPF_NETD_PATH "prog_netd_skfilter_denylist_xtbpf"
+
+#define ASSERT_STRING_EQUAL(s1, s2) \
+    static_assert(std::string_view(s1) == std::string_view(s2), "mismatch vs Android T netd")
+
+/* -=-=-=-=- WARNING -=-=-=-=-
+ *
+ * These 4 xt_bpf program paths are actually defined by:
+ *   //system/netd/include/binder_utils/XtBpfProgLocations.h
+ * which is intentionally a non-automerged location.
+ *
+ * They are *UNCHANGEABLE* due to being hard coded in Android T's netd binary
+ * as such we have compile time asserts that things match.
+ * (which will be validated during build on mainline-prod branch against old system/netd)
+ *
+ * If you break this, netd on T will fail to start with your tethering mainline module.
+ */
+ASSERT_STRING_EQUAL(XT_BPF_INGRESS_PROG_PATH,   BPF_NETD_PATH "prog_netd_skfilter_ingress_xtbpf");
+ASSERT_STRING_EQUAL(XT_BPF_EGRESS_PROG_PATH,    BPF_NETD_PATH "prog_netd_skfilter_egress_xtbpf");
+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 TC_BPF_INGRESS_ACCOUNT_PROG_NAME "prog_netd_schedact_ingress_account"
@@ -122,6 +146,8 @@
 #define UID_OWNER_MAP_PATH BPF_NETD_PATH "map_netd_uid_owner_map"
 #define UID_PERMISSION_MAP_PATH BPF_NETD_PATH "map_netd_uid_permission_map"
 
+#endif // __cplusplus
+
 enum UidOwnerMatchType {
     NO_MATCH = 0,
     HAPPY_BOX_MATCH = (1 << 0),
@@ -168,16 +194,6 @@
 // Entry in the configuration map that stores which stats map is currently in use.
 #define CURRENT_STATS_MAP_CONFIGURATION_KEY 2
 
-#define BPF_CLATD_PATH "/sys/fs/bpf/net_shared/"
-
-#define CLAT_INGRESS6_PROG_RAWIP_NAME "prog_clatd_schedcls_ingress6_clat_rawip"
-#define CLAT_INGRESS6_PROG_ETHER_NAME "prog_clatd_schedcls_ingress6_clat_ether"
-
-#define CLAT_INGRESS6_PROG_RAWIP_PATH BPF_CLATD_PATH CLAT_INGRESS6_PROG_RAWIP_NAME
-#define CLAT_INGRESS6_PROG_ETHER_PATH BPF_CLATD_PATH CLAT_INGRESS6_PROG_ETHER_NAME
-
-#define CLAT_INGRESS6_MAP_PATH BPF_CLATD_PATH "map_clatd_clat_ingress6_map"
-
 typedef struct {
     uint32_t iif;            // The input interface index
     struct in6_addr pfx96;   // The source /96 nat64 prefix, bottom 32 bits must be 0
@@ -191,14 +207,6 @@
 } ClatIngress6Value;
 STRUCT_SIZE(ClatIngress6Value, 4 + 4);  // 8
 
-#define CLAT_EGRESS4_PROG_RAWIP_NAME "prog_clatd_schedcls_egress4_clat_rawip"
-#define CLAT_EGRESS4_PROG_ETHER_NAME "prog_clatd_schedcls_egress4_clat_ether"
-
-#define CLAT_EGRESS4_PROG_RAWIP_PATH BPF_CLATD_PATH CLAT_EGRESS4_PROG_RAWIP_NAME
-#define CLAT_EGRESS4_PROG_ETHER_PATH BPF_CLATD_PATH CLAT_EGRESS4_PROG_ETHER_NAME
-
-#define CLAT_EGRESS4_MAP_PATH BPF_CLATD_PATH "map_clatd_clat_egress4_map"
-
 typedef struct {
     uint32_t iif;           // The input interface index
     struct in_addr local4;  // The source IPv4 address
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index 94d5ed8..8f72f7c 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -28,7 +28,6 @@
 #include <linux/ipv6.h>
 #include <linux/pkt_cls.h>
 #include <linux/tcp.h>
-#include <netdutils/UidConstants.h>
 #include <stdbool.h>
 #include <stdint.h>
 #include "bpf_net_helpers.h"
@@ -52,28 +51,35 @@
 #define TCP_FLAG_OFF 13
 #define RST_OFFSET 2
 
-DEFINE_BPF_MAP_GRW(cookie_tag_map, HASH, uint64_t, UidTagValue, COOKIE_UID_MAP_SIZE,
-                   AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(uid_counterset_map, HASH, uint32_t, uint8_t, UID_COUNTERSET_MAP_SIZE,
-                   AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(app_uid_stats_map, HASH, uint32_t, StatsValue, APP_STATS_MAP_SIZE,
-                   AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(stats_map_A, HASH, StatsKey, StatsValue, STATS_MAP_SIZE, AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(stats_map_B, HASH, StatsKey, StatsValue, STATS_MAP_SIZE, AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(iface_stats_map, HASH, uint32_t, StatsValue, IFACE_STATS_MAP_SIZE,
-                   AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(configuration_map, HASH, uint32_t, uint32_t, CONFIGURATION_MAP_SIZE,
-                   AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(uid_owner_map, HASH, uint32_t, UidOwnerValue, UID_OWNER_MAP_SIZE,
-                   AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(uid_permission_map, HASH, uint32_t, uint8_t, UID_OWNER_MAP_SIZE, AID_NET_BW_ACCT)
+// For maps netd does not need to access
+#define DEFINE_BPF_MAP_NO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
+    DEFINE_BPF_MAP_UGM(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, AID_ROOT, AID_NET_BW_ACCT, 0060)
+
+// For maps netd only needs read only access to
+#define DEFINE_BPF_MAP_RO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
+    DEFINE_BPF_MAP_UGM(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, AID_ROOT, AID_NET_BW_ACCT, 0460)
+
+// For maps netd needs to be able to read and write
+#define DEFINE_BPF_MAP_RW_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
+    DEFINE_BPF_MAP_UGM(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, AID_ROOT, AID_NET_BW_ACCT, 0660)
+
+DEFINE_BPF_MAP_RW_NETD(cookie_tag_map, HASH, uint64_t, UidTagValue, COOKIE_UID_MAP_SIZE)
+DEFINE_BPF_MAP_NO_NETD(uid_counterset_map, HASH, uint32_t, uint8_t, UID_COUNTERSET_MAP_SIZE)
+DEFINE_BPF_MAP_NO_NETD(app_uid_stats_map, HASH, uint32_t, StatsValue, APP_STATS_MAP_SIZE)
+DEFINE_BPF_MAP_RW_NETD(stats_map_A, HASH, StatsKey, StatsValue, STATS_MAP_SIZE)
+DEFINE_BPF_MAP_RO_NETD(stats_map_B, HASH, StatsKey, StatsValue, STATS_MAP_SIZE)
+DEFINE_BPF_MAP_NO_NETD(iface_stats_map, HASH, uint32_t, StatsValue, IFACE_STATS_MAP_SIZE)
+DEFINE_BPF_MAP_RW_NETD(configuration_map, HASH, uint32_t, uint32_t, CONFIGURATION_MAP_SIZE)
+DEFINE_BPF_MAP_NO_NETD(uid_owner_map, HASH, uint32_t, UidOwnerValue, UID_OWNER_MAP_SIZE)
+DEFINE_BPF_MAP_RW_NETD(uid_permission_map, HASH, uint32_t, uint8_t, UID_OWNER_MAP_SIZE)
 
 /* never actually used from ebpf */
-DEFINE_BPF_MAP_GRW(iface_index_name_map, HASH, uint32_t, IfaceValue, IFACE_INDEX_NAME_MAP_SIZE,
-                   AID_NET_BW_ACCT)
+DEFINE_BPF_MAP_NO_NETD(iface_index_name_map, HASH, uint32_t, IfaceValue, IFACE_INDEX_NAME_MAP_SIZE)
 
 static __always_inline int is_system_uid(uint32_t uid) {
-    return (uid <= MAX_SYSTEM_UID) && (uid >= MIN_SYSTEM_UID);
+    // MIN_SYSTEM_UID is AID_ROOT == 0, so uint32_t is *always* >= 0
+    // MAX_SYSTEM_UID is AID_NOBODY == 9999, while AID_APP_START == 10000
+    return (uid < AID_APP_START);
 }
 
 /*
@@ -189,6 +195,11 @@
     return *config;
 }
 
+// DROP_IF_SET is set of rules that BPF_DROP if rule is globally enabled, and per-uid bit is set
+#define DROP_IF_SET (STANDBY_MATCH | OEM_DENY_1_MATCH | OEM_DENY_2_MATCH | OEM_DENY_3_MATCH)
+// DROP_IF_UNSET is set of rules that should DROP if globally enabled, and per-uid bit is NOT set
+#define DROP_IF_UNSET (DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH | LOW_POWER_STANDBY_MATCH)
+
 static inline int bpf_owner_match(struct __sk_buff* skb, uint32_t uid, int direction) {
     if (skip_owner_match(skb)) return BPF_PASS;
 
@@ -200,32 +211,13 @@
     uint32_t uidRules = uidEntry ? uidEntry->rule : 0;
     uint32_t allowed_iif = uidEntry ? uidEntry->iif : 0;
 
-    if (enabledRules) {
-        if ((enabledRules & DOZABLE_MATCH) && !(uidRules & DOZABLE_MATCH)) {
-            return BPF_DROP;
-        }
-        if ((enabledRules & STANDBY_MATCH) && (uidRules & STANDBY_MATCH)) {
-            return BPF_DROP;
-        }
-        if ((enabledRules & POWERSAVE_MATCH) && !(uidRules & POWERSAVE_MATCH)) {
-            return BPF_DROP;
-        }
-        if ((enabledRules & RESTRICTED_MATCH) && !(uidRules & RESTRICTED_MATCH)) {
-            return BPF_DROP;
-        }
-        if ((enabledRules & LOW_POWER_STANDBY_MATCH) && !(uidRules & LOW_POWER_STANDBY_MATCH)) {
-            return BPF_DROP;
-        }
-        if ((enabledRules & OEM_DENY_1_MATCH) && (uidRules & OEM_DENY_1_MATCH)) {
-            return BPF_DROP;
-        }
-        if ((enabledRules & OEM_DENY_2_MATCH) && (uidRules & OEM_DENY_2_MATCH)) {
-            return BPF_DROP;
-        }
-        if ((enabledRules & OEM_DENY_3_MATCH) && (uidRules & OEM_DENY_3_MATCH)) {
-            return BPF_DROP;
-        }
-    }
+    // Warning: funky bit-wise arithmetic: in parallel, for all DROP_IF_SET/UNSET rules
+    // check whether the rules are globally enabled, and if so whether the rules are
+    // set/unset for the specific uid.  BPF_DROP if that is the case for ANY of the rules.
+    // We achieve this by masking out only the bits/rules we're interested in checking,
+    // and negating (via bit-wise xor) the bits/rules that should drop if unset.
+    if (enabledRules & (DROP_IF_SET | DROP_IF_UNSET) & (uidRules ^ DROP_IF_UNSET)) return BPF_DROP;
+
     if (direction == BPF_INGRESS && skb->ifindex != 1) {
         if (uidRules & IIF_MATCH) {
             if (allowed_iif && skb->ifindex != allowed_iif) {
@@ -326,6 +318,7 @@
     return bpf_traffic_account(skb, BPF_EGRESS);
 }
 
+// WARNING: Android T's non-updatable netd depends on the name of this program.
 DEFINE_BPF_PROG("skfilter/egress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_egress_prog)
 (struct __sk_buff* skb) {
     // Clat daemon does not generate new traffic, all its traffic is accounted for already
@@ -345,6 +338,7 @@
     return BPF_MATCH;
 }
 
+// WARNING: Android T's non-updatable netd depends on the name of this program.
 DEFINE_BPF_PROG("skfilter/ingress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_ingress_prog)
 (struct __sk_buff* skb) {
     // Clat daemon traffic is not accounted by virtue of iptables raw prerouting drop rule
@@ -367,6 +361,7 @@
     return TC_ACT_UNSPEC;
 }
 
+// WARNING: Android T's non-updatable netd depends on the name of this program.
 DEFINE_BPF_PROG("skfilter/allowlist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_allowlist_prog)
 (struct __sk_buff* skb) {
     uint32_t sock_uid = bpf_get_socket_uid(skb);
@@ -384,6 +379,7 @@
     return BPF_NOMATCH;
 }
 
+// WARNING: Android T's non-updatable netd depends on the name of this program.
 DEFINE_BPF_PROG("skfilter/denylist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_denylist_prog)
 (struct __sk_buff* skb) {
     uint32_t sock_uid = bpf_get_socket_uid(skb);
@@ -401,7 +397,7 @@
      * user at install time so we only check the appId part of a request uid at
      * run time. See UserHandle#isSameApp for detail.
      */
-    uint32_t appId = (gid_uid & 0xffffffff) % PER_USER_RANGE;
+    uint32_t appId = (gid_uid & 0xffffffff) % AID_USER_OFFSET;  // == PER_USER_RANGE == 100000
     uint8_t* permissions = bpf_uid_permission_map_lookup_elem(&appId);
     if (!permissions) {
         // UID not in map. Default to just INTERNET permission.
diff --git a/framework-t/src/android/net/NetworkStats.java b/framework-t/src/android/net/NetworkStats.java
index 51ff5ec..0bb98f8 100644
--- a/framework-t/src/android/net/NetworkStats.java
+++ b/framework-t/src/android/net/NetworkStats.java
@@ -1300,6 +1300,17 @@
     }
 
     /**
+     * Removes the interface name from all entries.
+     * This mutates the original structure in place.
+     * @hide
+     */
+    public void clearInterfaces() {
+        for (int i = 0; i < size; i++) {
+            iface[i] = null;
+        }
+    }
+
+    /**
      * Only keep entries that match all specified filters.
      *
      * <p>This mutates the original structure in place. After this method is called,
diff --git a/framework/Android.bp b/framework/Android.bp
index d7de439..24d8cca 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -64,7 +64,6 @@
         ":framework-connectivity-sources",
         ":net-utils-framework-common-srcs",
         ":framework-connectivity-api-shared-srcs",
-        ":framework-connectivity-javastream-protos",
     ],
     aidl: {
         generate_get_transaction_name: true,
@@ -90,6 +89,7 @@
         "modules-utils-backgroundthread",
         "modules-utils-build",
         "modules-utils-preconditions",
+        "framework-connectivity-javastream-protos",
     ],
     libs: [
         "app-compat-annotations",
@@ -197,28 +197,16 @@
     visibility: ["//frameworks/base"],
 }
 
-gensrcs {
+java_library {
     name: "framework-connectivity-javastream-protos",
-    depfile: true,
-
-    tools: [
-        "aprotoc",
-        "protoc-gen-javastream",
-        "soong_zip",
+    proto: {
+        type: "stream",
+    },
+    srcs: [":framework-connectivity-protos"],
+    installable: false,
+    sdk_version: "module_current",
+    min_sdk_version: "30",
+    apex_available: [
+        "com.android.tethering",
     ],
-
-    cmd: "mkdir -p $(genDir)/$(in) " +
-        "&& $(location aprotoc) " +
-        "  --plugin=$(location protoc-gen-javastream) " +
-        "  --dependency_out=$(depfile) " +
-        "  --javastream_out=$(genDir)/$(in) " +
-        "  -Iexternal/protobuf/src " +
-        "  -I . " +
-        "  $(in) " +
-        "&& $(location soong_zip) -jar -o $(out) -C $(genDir)/$(in) -D $(genDir)/$(in)",
-
-    srcs: [
-        ":framework-connectivity-protos",
-    ],
-    output_extension: "srcjar",
 }
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index ddac19d..a2a1ac0 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -51,6 +51,9 @@
     field public static final int BLOCKED_REASON_RESTRICTED_MODE = 8; // 0x8
     field public static final int FIREWALL_CHAIN_DOZABLE = 1; // 0x1
     field public static final int FIREWALL_CHAIN_LOW_POWER_STANDBY = 5; // 0x5
+    field public static final int FIREWALL_CHAIN_OEM_DENY_1 = 7; // 0x7
+    field public static final int FIREWALL_CHAIN_OEM_DENY_2 = 8; // 0x8
+    field public static final int FIREWALL_CHAIN_OEM_DENY_3 = 9; // 0x9
     field public static final int FIREWALL_CHAIN_POWERSAVE = 3; // 0x3
     field public static final int FIREWALL_CHAIN_RESTRICTED = 4; // 0x4
     field public static final int FIREWALL_CHAIN_STANDBY = 2; // 0x2
@@ -197,6 +200,8 @@
     method public int describeContents();
     method @NonNull public android.os.ParcelFileDescriptor getFileDescriptor();
     method @NonNull public String getInterfaceName();
+    method @Nullable public android.net.MacAddress getMacAddress();
+    method public int getMtu();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.net.TestNetworkInterface> CREATOR;
   }
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index db1d7e9..f1298ce 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -249,10 +249,10 @@
     method public void onValidationStatus(int, @Nullable android.net.Uri);
     method @NonNull public android.net.Network register();
     method public void sendAddDscpPolicy(@NonNull android.net.DscpPolicy);
-    method public final void sendLinkProperties(@NonNull android.net.LinkProperties);
-    method public final void sendNetworkCapabilities(@NonNull android.net.NetworkCapabilities);
-    method public final void sendNetworkScore(@NonNull android.net.NetworkScore);
-    method public final void sendNetworkScore(@IntRange(from=0, to=99) int);
+    method public void sendLinkProperties(@NonNull android.net.LinkProperties);
+    method public void sendNetworkCapabilities(@NonNull android.net.NetworkCapabilities);
+    method public void sendNetworkScore(@NonNull android.net.NetworkScore);
+    method public void sendNetworkScore(@IntRange(from=0, to=99) int);
     method public final void sendQosCallbackError(int, int);
     method public final void sendQosSessionAvailable(int, int, @NonNull android.net.QosSessionAttributes);
     method public final void sendQosSessionLost(int, int, int);
@@ -262,7 +262,7 @@
     method @Deprecated public void setLegacySubtype(int, @NonNull String);
     method public void setLingerDuration(@NonNull java.time.Duration);
     method public void setTeardownDelayMillis(@IntRange(from=0, to=0x1388) int);
-    method public final void setUnderlyingNetworks(@Nullable java.util.List<android.net.Network>);
+    method public void setUnderlyingNetworks(@Nullable java.util.List<android.net.Network>);
     method public void unregister();
     method public void unregisterAfterReplacement(@IntRange(from=0, to=0x1388) int);
     field public static final int DSCP_POLICY_STATUS_DELETED = 4; // 0x4
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index d1c202c..39cd7f3 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -987,6 +987,7 @@
      * Denylist of apps that will not have network access due to OEM-specific restrictions.
      * @hide
      */
+    @SystemApi(client = MODULE_LIBRARIES)
     public static final int FIREWALL_CHAIN_OEM_DENY_1 = 7;
 
     /**
@@ -994,6 +995,7 @@
      * Denylist of apps that will not have network access due to OEM-specific restrictions.
      * @hide
      */
+    @SystemApi(client = MODULE_LIBRARIES)
     public static final int FIREWALL_CHAIN_OEM_DENY_2 = 8;
 
     /**
@@ -1001,6 +1003,7 @@
      * Denylist of apps that will not have network access due to OEM-specific restrictions.
      * @hide
      */
+    @SystemApi(client = MODULE_LIBRARIES)
     public static final int FIREWALL_CHAIN_OEM_DENY_3 = 9;
 
     /** @hide */
diff --git a/framework/src/android/net/ITestNetworkManager.aidl b/framework/src/android/net/ITestNetworkManager.aidl
index 27d13c1..d18b931 100644
--- a/framework/src/android/net/ITestNetworkManager.aidl
+++ b/framework/src/android/net/ITestNetworkManager.aidl
@@ -29,8 +29,10 @@
  */
 interface ITestNetworkManager
 {
-    TestNetworkInterface createInterface(boolean isTun, boolean bringUp, in LinkAddress[] addrs,
-            in @nullable String iface);
+    TestNetworkInterface createInterface(boolean isTun, boolean hasCarrier, boolean bringUp,
+            in LinkAddress[] addrs, in @nullable String iface);
+
+    void setCarrierEnabled(in TestNetworkInterface iface, boolean enabled);
 
     void setupTestNetwork(in String iface, in LinkProperties lp, in boolean isMetered,
             in int[] administratorUids, in IBinder binder);
diff --git a/framework/src/android/net/NetworkAgent.java b/framework/src/android/net/NetworkAgent.java
index 2c50c73..5659a35 100644
--- a/framework/src/android/net/NetworkAgent.java
+++ b/framework/src/android/net/NetworkAgent.java
@@ -913,7 +913,7 @@
      * Must be called by the agent when the network's {@link LinkProperties} change.
      * @param linkProperties the new LinkProperties.
      */
-    public final void sendLinkProperties(@NonNull LinkProperties linkProperties) {
+    public void sendLinkProperties(@NonNull LinkProperties linkProperties) {
         Objects.requireNonNull(linkProperties);
         final LinkProperties lp = new LinkProperties(linkProperties);
         queueOrSendMessage(reg -> reg.sendLinkProperties(lp));
@@ -938,7 +938,7 @@
      * @param underlyingNetworks the new list of underlying networks.
      * @see {@link VpnService.Builder#setUnderlyingNetworks(Network[])}
      */
-    public final void setUnderlyingNetworks(
+    public void setUnderlyingNetworks(
             @SuppressLint("NullableCollection") @Nullable List<Network> underlyingNetworks) {
         final ArrayList<Network> underlyingArray = (underlyingNetworks != null)
                 ? new ArrayList<>(underlyingNetworks) : null;
@@ -1088,7 +1088,7 @@
      * Must be called by the agent when the network's {@link NetworkCapabilities} change.
      * @param networkCapabilities the new NetworkCapabilities.
      */
-    public final void sendNetworkCapabilities(@NonNull NetworkCapabilities networkCapabilities) {
+    public void sendNetworkCapabilities(@NonNull NetworkCapabilities networkCapabilities) {
         Objects.requireNonNull(networkCapabilities);
         mBandwidthUpdatePending.set(false);
         mLastBwRefreshTime = System.currentTimeMillis();
@@ -1102,7 +1102,7 @@
      *
      * @param score the new score.
      */
-    public final void sendNetworkScore(@NonNull NetworkScore score) {
+    public void sendNetworkScore(@NonNull NetworkScore score) {
         Objects.requireNonNull(score);
         queueOrSendMessage(reg -> reg.sendScore(score));
     }
@@ -1113,7 +1113,7 @@
      * @param score the new score, between 0 and 99.
      * deprecated use sendNetworkScore(NetworkScore) TODO : remove in S.
      */
-    public final void sendNetworkScore(@IntRange(from = 0, to = 99) int score) {
+    public void sendNetworkScore(@IntRange(from = 0, to = 99) int score) {
         sendNetworkScore(new NetworkScore.Builder().setLegacyInt(score).build());
     }
 
diff --git a/framework/src/android/net/TestNetworkInterface.java b/framework/src/android/net/TestNetworkInterface.java
index 4449ff8..26200e1 100644
--- a/framework/src/android/net/TestNetworkInterface.java
+++ b/framework/src/android/net/TestNetworkInterface.java
@@ -16,22 +16,32 @@
 package android.net;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
 import android.os.Parcelable;
+import android.util.Log;
+
+import java.net.NetworkInterface;
+import java.net.SocketException;
 
 /**
- * This class is used to return the interface name and fd of the test interface
+ * This class is used to return the interface name, fd, MAC, and MTU of the test interface
  *
  * @hide
  */
 @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
 public final class TestNetworkInterface implements Parcelable {
+    private static final String TAG = "TestNetworkInterface";
+
     @NonNull
     private final ParcelFileDescriptor mFileDescriptor;
     @NonNull
     private final String mInterfaceName;
+    @Nullable
+    private final MacAddress mMacAddress;
+    private final int mMtu;
 
     @Override
     public int describeContents() {
@@ -40,18 +50,41 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel out, int flags) {
-        out.writeParcelable(mFileDescriptor, PARCELABLE_WRITE_RETURN_VALUE);
+        out.writeParcelable(mFileDescriptor, flags);
         out.writeString(mInterfaceName);
+        out.writeParcelable(mMacAddress, flags);
+        out.writeInt(mMtu);
     }
 
     public TestNetworkInterface(@NonNull ParcelFileDescriptor pfd, @NonNull String intf) {
         mFileDescriptor = pfd;
         mInterfaceName = intf;
+
+        MacAddress macAddress = null;
+        int mtu = 1500;
+        try {
+            // This constructor is called by TestNetworkManager which runs inside the system server,
+            // which has permission to read the MacAddress.
+            NetworkInterface nif = NetworkInterface.getByName(mInterfaceName);
+
+            // getHardwareAddress() returns null for tun interfaces.
+            byte[] hardwareAddress = nif.getHardwareAddress();
+            if (hardwareAddress != null) {
+                macAddress = MacAddress.fromBytes(nif.getHardwareAddress());
+            }
+            mtu = nif.getMTU();
+        } catch (SocketException e) {
+            Log.e(TAG, "Failed to fetch MacAddress or MTU size from NetworkInterface", e);
+        }
+        mMacAddress = macAddress;
+        mMtu = mtu;
     }
 
     private TestNetworkInterface(@NonNull Parcel in) {
         mFileDescriptor = in.readParcelable(ParcelFileDescriptor.class.getClassLoader());
         mInterfaceName = in.readString();
+        mMacAddress = in.readParcelable(MacAddress.class.getClassLoader());
+        mMtu = in.readInt();
     }
 
     @NonNull
@@ -64,6 +97,15 @@
         return mInterfaceName;
     }
 
+    @Nullable
+    public MacAddress getMacAddress() {
+        return mMacAddress;
+    }
+
+    public int getMtu() {
+        return mMtu;
+    }
+
     @NonNull
     public static final Parcelable.Creator<TestNetworkInterface> CREATOR =
             new Parcelable.Creator<TestNetworkInterface>() {
diff --git a/framework/src/android/net/TestNetworkManager.java b/framework/src/android/net/TestNetworkManager.java
index 4e78823..7b18765 100644
--- a/framework/src/android/net/TestNetworkManager.java
+++ b/framework/src/android/net/TestNetworkManager.java
@@ -58,6 +58,7 @@
     private static final boolean TAP = false;
     private static final boolean TUN = true;
     private static final boolean BRING_UP = true;
+    private static final boolean CARRIER_UP = true;
     private static final LinkAddress[] NO_ADDRS = new LinkAddress[0];
 
     /** @hide */
@@ -166,7 +167,7 @@
     public TestNetworkInterface createTunInterface(@NonNull Collection<LinkAddress> linkAddrs) {
         try {
             final LinkAddress[] arr = new LinkAddress[linkAddrs.size()];
-            return mService.createInterface(TUN, BRING_UP, linkAddrs.toArray(arr),
+            return mService.createInterface(TUN, CARRIER_UP, BRING_UP, linkAddrs.toArray(arr),
                     null /* iface */);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
@@ -185,7 +186,7 @@
     @NonNull
     public TestNetworkInterface createTapInterface() {
         try {
-            return mService.createInterface(TAP, BRING_UP, NO_ADDRS, null /* iface */);
+            return mService.createInterface(TAP, CARRIER_UP, BRING_UP, NO_ADDRS, null /* iface */);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -204,7 +205,7 @@
     @NonNull
     public TestNetworkInterface createTapInterface(boolean bringUp) {
         try {
-            return mService.createInterface(TAP, bringUp, NO_ADDRS, null /* iface */);
+            return mService.createInterface(TAP, CARRIER_UP, bringUp, NO_ADDRS, null /* iface */);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -226,7 +227,43 @@
     @NonNull
     public TestNetworkInterface createTapInterface(boolean bringUp, @NonNull String iface) {
         try {
-            return mService.createInterface(TAP, bringUp, NO_ADDRS, iface);
+            return mService.createInterface(TAP, CARRIER_UP, bringUp, NO_ADDRS, iface);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Create a tap interface with or without carrier for testing purposes.
+     *
+     * @param carrierUp whether the created interface has a carrier or not.
+     * @param bringUp whether to bring up the interface before returning it.
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
+    @NonNull
+    public TestNetworkInterface createTapInterface(boolean carrierUp, boolean bringUp) {
+        try {
+            return mService.createInterface(TAP, carrierUp, bringUp, NO_ADDRS, null /* iface */);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Enable / disable carrier on TestNetworkInterface
+     *
+     * Note: TUNSETCARRIER is not supported until kernel version 5.0.
+     * TODO: add RequiresApi annotation.
+     *
+     * @param iface the interface to configure.
+     * @param enabled true to turn carrier on, false to turn carrier off.
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
+    public void setCarrierEnabled(@NonNull TestNetworkInterface iface, boolean enabled) {
+        try {
+            mService.setCarrierEnabled(iface, enabled);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/netd/BpfHandler.cpp b/netd/BpfHandler.cpp
index 42d0de5..5ae8ab6 100644
--- a/netd/BpfHandler.cpp
+++ b/netd/BpfHandler.cpp
@@ -199,6 +199,7 @@
 
     BpfMap<StatsKey, StatsValue>& currentMap =
             (configuration.value() == SELECT_MAP_A) ? mStatsMapA : mStatsMapB;
+    // HACK: mStatsMapB becomes RW BpfMap here, but countUidStatsEntries doesn't modify so it works
     base::Result<void> res = currentMap.iterate(countUidStatsEntries);
     if (!res.ok()) {
         ALOGE("Failed to count the stats entry in map %d: %s", currentMap.getMap().get(),
diff --git a/netd/BpfHandler.h b/netd/BpfHandler.h
index 05b9ebc..7e3b94d 100644
--- a/netd/BpfHandler.h
+++ b/netd/BpfHandler.h
@@ -23,6 +23,7 @@
 #include "bpf_shared.h"
 
 using android::bpf::BpfMap;
+using android::bpf::BpfMapRO;
 
 namespace android {
 namespace net {
@@ -61,7 +62,7 @@
 
     BpfMap<uint64_t, UidTagValue> mCookieTagMap;
     BpfMap<StatsKey, StatsValue> mStatsMapA;
-    BpfMap<StatsKey, StatsValue> mStatsMapB;
+    BpfMapRO<StatsKey, StatsValue> mStatsMapB;
     BpfMap<uint32_t, uint32_t> mConfigurationMap;
     BpfMap<uint32_t, uint8_t> mUidPermissionMap;
 
diff --git a/netd/BpfHandlerTest.cpp b/netd/BpfHandlerTest.cpp
index 1bd222d..c0f7e45 100644
--- a/netd/BpfHandlerTest.cpp
+++ b/netd/BpfHandlerTest.cpp
@@ -21,7 +21,7 @@
 
 #include <gtest/gtest.h>
 
-#define TEST_BPF_MAP
+#define BPF_MAP_MAKE_VISIBLE_FOR_TESTING
 #include "BpfHandler.h"
 
 using namespace android::bpf;  // NOLINT(google-build-using-namespace): exempted
@@ -65,7 +65,7 @@
         mFakeConfigurationMap.resetMap(BPF_MAP_TYPE_HASH, 1);
         ASSERT_VALID(mFakeConfigurationMap);
 
-        mFakeUidPermissionMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+        mFakeUidPermissionMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
         ASSERT_VALID(mFakeUidPermissionMap);
 
         mBh.mCookieTagMap = mFakeCookieTagMap;
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
index 4974b96..6f9c8c2 100644
--- a/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
@@ -33,6 +33,7 @@
 #include <android-base/stringprintf.h>
 #include <android-base/strings.h>
 
+#define BPF_MAP_MAKE_VISIBLE_FOR_TESTING
 #include "bpf/BpfMap.h"
 #include "bpf/BpfUtils.h"
 #include "netdbpf/BpfNetworkStats.h"
@@ -80,19 +81,19 @@
         ASSERT_EQ(0, setrlimitForTest());
 
         mFakeCookieTagMap = BpfMap<uint64_t, UidTagValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
-        ASSERT_LE(0, mFakeCookieTagMap.getMap());
+        ASSERT_TRUE(mFakeCookieTagMap.isValid());
 
         mFakeAppUidStatsMap = BpfMap<uint32_t, StatsValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
-        ASSERT_LE(0, mFakeAppUidStatsMap.getMap());
+        ASSERT_TRUE(mFakeAppUidStatsMap.isValid());
 
         mFakeStatsMap = BpfMap<StatsKey, StatsValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
-        ASSERT_LE(0, mFakeStatsMap.getMap());
+        ASSERT_TRUE(mFakeStatsMap.isValid());
 
         mFakeIfaceIndexNameMap = BpfMap<uint32_t, IfaceValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
-        ASSERT_LE(0, mFakeIfaceIndexNameMap.getMap());
+        ASSERT_TRUE(mFakeIfaceIndexNameMap.isValid());
 
         mFakeIfaceStatsMap = BpfMap<uint32_t, StatsValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
-        ASSERT_LE(0, mFakeIfaceStatsMap.getMap());
+        ASSERT_TRUE(mFakeIfaceStatsMap.isValid());
     }
 
     void expectUidTag(uint64_t cookie, uid_t uid, uint32_t tag) {
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index cd29185..ff6e45d 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -25,6 +25,7 @@
 import static android.content.Intent.ACTION_USER_REMOVED;
 import static android.content.Intent.EXTRA_UID;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.NetworkStats.DEFAULT_NETWORK_ALL;
 import static android.net.NetworkStats.IFACE_ALL;
@@ -172,6 +173,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Semaphore;
@@ -341,11 +343,16 @@
     @GuardedBy("mStatsLock")
     private String mActiveIface;
 
-    /** Set of any ifaces associated with mobile networks since boot. */
+    /** Set of all ifaces currently associated with mobile networks. */
     private volatile String[] mMobileIfaces = new String[0];
 
-    /** Set of any ifaces associated with wifi networks since boot. */
-    private volatile String[] mWifiIfaces = new String[0];
+    /* A set of all interfaces that have ever been associated with mobile networks since boot. */
+    @GuardedBy("mStatsLock")
+    private final Set<String> mAllMobileIfacesSinceBoot = new ArraySet<>();
+
+    /* A set of all interfaces that have ever been associated with wifi networks since boot. */
+    @GuardedBy("mStatsLock")
+    private final Set<String> mAllWifiIfacesSinceBoot = new ArraySet<>();
 
     /** Set of all ifaces currently used by traffic that does not explicitly specify a Network. */
     @GuardedBy("mStatsLock")
@@ -1468,9 +1475,9 @@
         // We've been using pure XT stats long enough that we no longer need to
         // splice DEV and XT together.
         final NetworkStatsHistory history = internalGetHistoryForNetwork(template, flags, FIELD_ALL,
-                accessLevel, callingUid, start, end);
+                accessLevel, callingUid, Long.MIN_VALUE, Long.MAX_VALUE);
 
-        final long now = System.currentTimeMillis();
+        final long now = mClock.millis();
         final NetworkStatsHistory.Entry entry = history.getValues(start, end, now, null);
 
         final NetworkStats stats = new NetworkStats(end - start, 1);
@@ -1551,17 +1558,37 @@
         return dataLayer;
     }
 
+    private String[] getAllIfacesSinceBoot(int transport) {
+        synchronized (mStatsLock) {
+            final Set<String> ifaceSet;
+            if (transport == TRANSPORT_WIFI) {
+                ifaceSet = mAllWifiIfacesSinceBoot;
+            } else if (transport == TRANSPORT_CELLULAR) {
+                ifaceSet = mAllMobileIfacesSinceBoot;
+            } else {
+                throw new IllegalArgumentException("Invalid transport " + transport);
+            }
+
+            return ifaceSet.toArray(new String[0]);
+        }
+    }
+
     @Override
     public NetworkStats getUidStatsForTransport(int transport) {
         PermissionUtils.enforceNetworkStackPermission(mContext);
         try {
-            final String[] relevantIfaces =
-                    transport == TRANSPORT_WIFI ? mWifiIfaces : mMobileIfaces;
+            final String[] ifaceArray = getAllIfacesSinceBoot(transport);
             // TODO(b/215633405) : mMobileIfaces and mWifiIfaces already contain the stacked
             // interfaces, so this is not useful, remove it.
             final String[] ifacesToQuery =
-                    mStatsFactory.augmentWithStackedInterfaces(relevantIfaces);
-            return getNetworkStatsUidDetail(ifacesToQuery);
+                    mStatsFactory.augmentWithStackedInterfaces(ifaceArray);
+            final NetworkStats stats = getNetworkStatsUidDetail(ifacesToQuery);
+            // Clear the interfaces of the stats before returning, so callers won't get this
+            // information. This is because no caller needs this information for now, and it
+            // makes it easier to change the implementation later by using the histories in the
+            // recorder.
+            stats.clearInterfaces();
+            return stats;
         } catch (RemoteException e) {
             Log.wtf(TAG, "Error compiling UID stats", e);
             return new NetworkStats(0L, 0);
@@ -1570,11 +1597,6 @@
 
     @Override
     public String[] getMobileIfaces() {
-        // TODO (b/192758557): Remove debug log.
-        if (CollectionUtils.contains(mMobileIfaces, null)) {
-            throw new NullPointerException(
-                    "null element in mMobileIfaces: " + Arrays.toString(mMobileIfaces));
-        }
         return mMobileIfaces.clone();
     }
 
@@ -1942,7 +1964,6 @@
 
         final boolean combineSubtypeEnabled = mSettings.getCombineSubtypeEnabled();
         final ArraySet<String> mobileIfaces = new ArraySet<>();
-        final ArraySet<String> wifiIfaces = new ArraySet<>();
         for (NetworkStateSnapshot snapshot : snapshots) {
             final int displayTransport =
                     getDisplayTransport(snapshot.getNetworkCapabilities().getTransportTypes());
@@ -1987,9 +2008,12 @@
 
                 if (isMobile) {
                     mobileIfaces.add(baseIface);
+                    // If the interface name was present in the wifi set, the interface won't
+                    // be removed from it to prevent stats from getting rollback.
+                    mAllMobileIfacesSinceBoot.add(baseIface);
                 }
                 if (isWifi) {
-                    wifiIfaces.add(baseIface);
+                    mAllWifiIfacesSinceBoot.add(baseIface);
                 }
             }
 
@@ -2031,9 +2055,10 @@
                     findOrCreateNetworkIdentitySet(mActiveUidIfaces, iface).add(ident);
                     if (isMobile) {
                         mobileIfaces.add(iface);
+                        mAllMobileIfacesSinceBoot.add(iface);
                     }
                     if (isWifi) {
-                        wifiIfaces.add(iface);
+                        mAllWifiIfacesSinceBoot.add(iface);
                     }
 
                     mStatsFactory.noteStackedIface(iface, baseIface);
@@ -2042,16 +2067,6 @@
         }
 
         mMobileIfaces = mobileIfaces.toArray(new String[0]);
-        mWifiIfaces = wifiIfaces.toArray(new String[0]);
-        // TODO (b/192758557): Remove debug log.
-        if (CollectionUtils.contains(mMobileIfaces, null)) {
-            throw new NullPointerException(
-                    "null element in mMobileIfaces: " + Arrays.toString(mMobileIfaces));
-        }
-        if (CollectionUtils.contains(mWifiIfaces, null)) {
-            throw new NullPointerException(
-                    "null element in mWifiIfaces: " + Arrays.toString(mWifiIfaces));
-        }
     }
 
     private static int getSubIdForMobile(@NonNull NetworkStateSnapshot state) {
@@ -2516,6 +2531,22 @@
             }
             pw.decreaseIndent();
 
+            pw.println("All wifi interfaces:");
+            pw.increaseIndent();
+            for (String iface : mAllWifiIfacesSinceBoot) {
+                pw.print(iface + " ");
+            }
+            pw.println();
+            pw.decreaseIndent();
+
+            pw.println("All mobile interfaces:");
+            pw.increaseIndent();
+            for (String iface : mAllMobileIfacesSinceBoot) {
+                pw.print(iface + " ");
+            }
+            pw.println();
+            pw.decreaseIndent();
+
             // Get the top openSession callers
             final HashMap calls;
             synchronized (mOpenSessionCallsLock) {
diff --git a/service/Android.bp b/service/Android.bp
index 91b9d1c..45e43bc 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -198,11 +198,10 @@
     lint: { strict_updatability_linting: true },
 }
 
-java_library {
-    name: "service-connectivity",
+java_defaults {
+    name: "service-connectivity-defaults",
     sdk_version: "system_server_current",
     min_sdk_version: "30",
-    installable: true,
     // This library combines system server jars that have access to different bootclasspath jars.
     // Lower SDK service jars must not depend on higher SDK jars as that would let them
     // transitively depend on the wrong bootclasspath jars. Sources also cannot be added here as
@@ -224,6 +223,24 @@
     lint: { strict_updatability_linting: true },
 }
 
+// A special library created strictly for use by the tests as they need the
+// implementation library but that is not available when building from prebuilts.
+// Using a library with a different name to what is used by the prebuilts ensures
+// that this will never depend on the prebuilt.
+// Switching service-connectivity to a java_sdk_library would also have worked as
+// that has built in support for managing this but that is too big a change at this
+// point.
+java_library {
+    name: "service-connectivity-for-tests",
+    defaults: ["service-connectivity-defaults"],
+}
+
+java_library {
+    name: "service-connectivity",
+    defaults: ["service-connectivity-defaults"],
+    installable: true,
+}
+
 filegroup {
     name: "connectivity-jarjar-rules",
     srcs: ["jarjar-rules.txt"],
diff --git a/service/jarjar-rules.txt b/service/jarjar-rules.txt
index c7223fc..4013d2e 100644
--- a/service/jarjar-rules.txt
+++ b/service/jarjar-rules.txt
@@ -118,6 +118,7 @@
 rule androidx.core.** com.android.server.nearby.@0
 rule androidx.versionedparcelable.** com.android.server.nearby.@0
 rule com.google.common.** com.android.server.nearby.@0
+rule android.support.v4.** com.android.server.nearby.@0
 
 # Remaining are connectivity sources in com.android.server and com.android.server.connectivity:
 # TODO: move to a subpackage of com.android.connectivity (such as com.android.connectivity.server)
diff --git a/service/jni/com_android_server_TestNetworkService.cpp b/service/jni/com_android_server_TestNetworkService.cpp
index 4efd0e1..9c7a761 100644
--- a/service/jni/com_android_server_TestNetworkService.cpp
+++ b/service/jni/com_android_server_TestNetworkService.cpp
@@ -51,7 +51,15 @@
     jniThrowException(env, "java/lang/IllegalStateException", msg.c_str());
 }
 
-static int createTunTapInterface(JNIEnv* env, bool isTun, const char* iface) {
+// enable or disable  carrier on tun / tap interface.
+static void setTunTapCarrierEnabledImpl(JNIEnv* env, const char* iface, int tunFd, bool enabled) {
+    uint32_t carrierOn = enabled;
+    if (ioctl(tunFd, TUNSETCARRIER, &carrierOn)) {
+        throwException(env, errno, "set carrier", iface);
+    }
+}
+
+static int createTunTapImpl(JNIEnv* env, bool isTun, bool hasCarrier, const char* iface) {
     base::unique_fd tun(open("/dev/tun", O_RDWR | O_NONBLOCK));
     ifreq ifr{};
 
@@ -63,6 +71,11 @@
         return -1;
     }
 
+    if (!hasCarrier) {
+        // disable carrier before setting IFF_UP
+        setTunTapCarrierEnabledImpl(env, iface, tun.get(), hasCarrier);
+    }
+
     // Activate interface using an unconnected datagram socket.
     base::unique_fd inet6CtrlSock(socket(AF_INET6, SOCK_DGRAM, 0));
     ifr.ifr_flags = IFF_UP;
@@ -79,23 +92,31 @@
 
 //------------------------------------------------------------------------------
 
-static jint create(JNIEnv* env, jobject /* thiz */, jboolean isTun, jstring jIface) {
+static void setTunTapCarrierEnabled(JNIEnv* env, jclass /* clazz */, jstring
+                                    jIface, jint tunFd, jboolean enabled) {
+    ScopedUtfChars iface(env, jIface);
+    if (!iface.c_str()) {
+        jniThrowNullPointerException(env, "iface");
+    }
+    setTunTapCarrierEnabledImpl(env, iface.c_str(), tunFd, enabled);
+}
+
+static jint createTunTap(JNIEnv* env, jclass /* clazz */, jboolean isTun,
+                             jboolean hasCarrier, jstring jIface) {
     ScopedUtfChars iface(env, jIface);
     if (!iface.c_str()) {
         jniThrowNullPointerException(env, "iface");
         return -1;
     }
 
-    int tun = createTunTapInterface(env, isTun, iface.c_str());
-
-    // Any exceptions will be thrown from the createTunTapInterface call
-    return tun;
+    return createTunTapImpl(env, isTun, hasCarrier, iface.c_str());
 }
 
 //------------------------------------------------------------------------------
 
 static const JNINativeMethod gMethods[] = {
-    {"jniCreateTunTap", "(ZLjava/lang/String;)I", (void*)create},
+    {"nativeSetTunTapCarrierEnabled", "(Ljava/lang/String;IZ)V", (void*)setTunTapCarrierEnabled},
+    {"nativeCreateTunTap", "(ZZLjava/lang/String;)I", (void*)createTunTap},
 };
 
 int register_com_android_server_TestNetworkService(JNIEnv* env) {
diff --git a/service/native/TrafficController.cpp b/service/native/TrafficController.cpp
index 8ad42c7..adc1925 100644
--- a/service/native/TrafficController.cpp
+++ b/service/native/TrafficController.cpp
@@ -495,8 +495,6 @@
               oldConfigure.error().message().c_str());
         return -oldConfigure.error().code();
     }
-    Status res;
-    BpfConfig newConfiguration;
     uint32_t match;
     switch (chain) {
         case DOZABLE:
@@ -526,9 +524,9 @@
         default:
             return -EINVAL;
     }
-    newConfiguration =
-            enable ? (oldConfigure.value() | match) : (oldConfigure.value() & (~match));
-    res = mConfigurationMap.writeValue(key, newConfiguration, BPF_EXIST);
+    BpfConfig newConfiguration =
+            enable ? (oldConfigure.value() | match) : (oldConfigure.value() & ~match);
+    Status res = mConfigurationMap.writeValue(key, newConfiguration, BPF_EXIST);
     if (!isOk(res)) {
         ALOGE("Failed to toggleUidOwnerMap(%d): %s", chain, res.msg().c_str());
     }
diff --git a/service/native/TrafficControllerTest.cpp b/service/native/TrafficControllerTest.cpp
index 0b4550e..b77c465 100644
--- a/service/native/TrafficControllerTest.cpp
+++ b/service/native/TrafficControllerTest.cpp
@@ -38,7 +38,7 @@
 
 #include <netdutils/MockSyscalls.h>
 
-#define TEST_BPF_MAP
+#define BPF_MAP_MAKE_VISIBLE_FOR_TESTING
 #include "TrafficController.h"
 #include "bpf/BpfUtils.h"
 #include "NetdUpdatablePublic.h"
@@ -68,6 +68,7 @@
 constexpr int TXBYTES = 0;
 
 #define ASSERT_VALID(x) ASSERT_TRUE((x).isValid())
+#define ASSERT_INVALID(x) ASSERT_FALSE((x).isValid())
 
 class TrafficControllerTest : public ::testing::Test {
   protected:
@@ -76,6 +77,8 @@
     BpfMap<uint64_t, UidTagValue> mFakeCookieTagMap;
     BpfMap<uint32_t, StatsValue> mFakeAppUidStatsMap;
     BpfMap<StatsKey, StatsValue> mFakeStatsMapA;
+    BpfMap<StatsKey, StatsValue> mFakeStatsMapB;  // makeTrafficControllerMapsInvalid only
+    BpfMap<uint32_t, StatsValue> mFakeIfaceStatsMap; ;  // makeTrafficControllerMapsInvalid only
     BpfMap<uint32_t, uint32_t> mFakeConfigurationMap;
     BpfMap<uint32_t, UidOwnerValue> mFakeUidOwnerMap;
     BpfMap<uint32_t, uint8_t> mFakeUidPermissionMap;
@@ -259,37 +262,6 @@
         EXPECT_TRUE(mTc.mPrivilegedUser.empty());
     }
 
-    void addPrivilegedUid(uid_t uid) {
-        std::vector privilegedUid = {uid};
-        mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, privilegedUid);
-    }
-
-    void removePrivilegedUid(uid_t uid) {
-        std::vector privilegedUid = {uid};
-        mTc.setPermissionForUids(INetd::PERMISSION_NONE, privilegedUid);
-    }
-
-    void expectFakeStatsUnchanged(uint64_t cookie, uint32_t tag, uint32_t uid,
-                                  StatsKey tagStatsMapKey) {
-        Result<UidTagValue> cookieMapResult = mFakeCookieTagMap.readValue(cookie);
-        EXPECT_RESULT_OK(cookieMapResult);
-        EXPECT_EQ(uid, cookieMapResult.value().uid);
-        EXPECT_EQ(tag, cookieMapResult.value().tag);
-        Result<StatsValue> statsMapResult = mFakeStatsMapA.readValue(tagStatsMapKey);
-        EXPECT_RESULT_OK(statsMapResult);
-        EXPECT_EQ((uint64_t)RXPACKETS, statsMapResult.value().rxPackets);
-        EXPECT_EQ((uint64_t)RXBYTES, statsMapResult.value().rxBytes);
-        tagStatsMapKey.tag = 0;
-        statsMapResult = mFakeStatsMapA.readValue(tagStatsMapKey);
-        EXPECT_RESULT_OK(statsMapResult);
-        EXPECT_EQ((uint64_t)RXPACKETS, statsMapResult.value().rxPackets);
-        EXPECT_EQ((uint64_t)RXBYTES, statsMapResult.value().rxBytes);
-        auto appStatsResult = mFakeAppUidStatsMap.readValue(uid);
-        EXPECT_RESULT_OK(appStatsResult);
-        EXPECT_EQ((uint64_t)RXPACKETS, appStatsResult.value().rxPackets);
-        EXPECT_EQ((uint64_t)RXBYTES, appStatsResult.value().rxBytes);
-    }
-
     Status updateUidOwnerMaps(const std::vector<uint32_t>& appUids,
                               UidOwnerMatchType matchType, TrafficController::IptOp op) {
         Status ret(0);
@@ -356,6 +328,52 @@
         }
         return true;
     }
+
+    // Once called, the maps of TrafficController can't recover to valid maps which initialized
+    // in SetUp().
+    void makeTrafficControllerMapsInvalid() {
+        constexpr char INVALID_PATH[] = "invalid";
+
+        mFakeCookieTagMap.init(INVALID_PATH);
+        mTc.mCookieTagMap = mFakeCookieTagMap;
+        ASSERT_INVALID(mTc.mCookieTagMap);
+
+        mFakeAppUidStatsMap.init(INVALID_PATH);
+        mTc.mAppUidStatsMap = mFakeAppUidStatsMap;
+        ASSERT_INVALID(mTc.mAppUidStatsMap);
+
+        mFakeStatsMapA.init(INVALID_PATH);
+        mTc.mStatsMapA = mFakeStatsMapA;
+        ASSERT_INVALID(mTc.mStatsMapA);
+
+        mFakeStatsMapB.init(INVALID_PATH);
+        mTc.mStatsMapB = mFakeStatsMapB;
+        ASSERT_INVALID(mTc.mStatsMapB);
+
+        mFakeIfaceStatsMap.init(INVALID_PATH);
+        mTc.mIfaceStatsMap = mFakeIfaceStatsMap;
+        ASSERT_INVALID(mTc.mIfaceStatsMap);
+
+        mFakeConfigurationMap.init(INVALID_PATH);
+        mTc.mConfigurationMap = mFakeConfigurationMap;
+        ASSERT_INVALID(mTc.mConfigurationMap);
+
+        mFakeUidOwnerMap.init(INVALID_PATH);
+        mTc.mUidOwnerMap = mFakeUidOwnerMap;
+        ASSERT_INVALID(mTc.mUidOwnerMap);
+
+        mFakeUidPermissionMap.init(INVALID_PATH);
+        mTc.mUidPermissionMap = mFakeUidPermissionMap;
+        ASSERT_INVALID(mTc.mUidPermissionMap);
+
+        mFakeUidCounterSetMap.init(INVALID_PATH);
+        mTc.mUidCounterSetMap = mFakeUidCounterSetMap;
+        ASSERT_INVALID(mTc.mUidCounterSetMap);
+
+        mFakeIfaceIndexNameMap.init(INVALID_PATH);
+        mTc.mIfaceIndexNameMap = mFakeIfaceIndexNameMap;
+        ASSERT_INVALID(mTc.mIfaceIndexNameMap);
+    }
 };
 
 TEST_F(TrafficControllerTest, TestUpdateOwnerMapEntry) {
@@ -804,6 +822,71 @@
     EXPECT_TRUE(expectDumpsysContains(expectedLines));
 }
 
+TEST_F(TrafficControllerTest, dumpsysInvalidMaps) {
+    makeTrafficControllerMapsInvalid();
+
+    const std::string kErrIterate = "print end with error: Get firstKey map -1 failed: "
+            "Bad file descriptor";
+    const std::string kErrReadRulesConfig = "read ownerMatch configure failed with error: "
+            "Read value of map -1 failed: Bad file descriptor";
+    const std::string kErrReadStatsMapConfig = "read stats map configure failed with error: "
+            "Read value of map -1 failed: Bad file descriptor";
+
+    std::vector<std::string> expectedLines = {
+        fmt::format("mCookieTagMap {}", kErrIterate),
+        fmt::format("mUidCounterSetMap {}", kErrIterate),
+        fmt::format("mAppUidStatsMap {}", kErrIterate),
+        fmt::format("mStatsMapA {}", kErrIterate),
+        fmt::format("mStatsMapB {}", kErrIterate),
+        fmt::format("mIfaceIndexNameMap {}", kErrIterate),
+        fmt::format("mIfaceStatsMap {}", kErrIterate),
+        fmt::format("mConfigurationMap {}", kErrReadRulesConfig),
+        fmt::format("mConfigurationMap {}", kErrReadStatsMapConfig),
+        fmt::format("mUidOwnerMap {}", kErrIterate),
+        fmt::format("mUidPermissionMap {}", kErrIterate)};
+    EXPECT_TRUE(expectDumpsysContains(expectedLines));
+}
+
+TEST_F(TrafficControllerTest, uidMatchTypeToString) {
+    // NO_MATCH(0) can't be verified because match type flag is added by OR operator.
+    // See TrafficController::addRule()
+    static const struct TestConfig {
+        UidOwnerMatchType uidOwnerMatchType;
+        std::string expected;
+    } testConfigs[] = {
+            // clang-format off
+            {HAPPY_BOX_MATCH, "HAPPY_BOX_MATCH"},
+            {DOZABLE_MATCH, "DOZABLE_MATCH"},
+            {STANDBY_MATCH, "STANDBY_MATCH"},
+            {POWERSAVE_MATCH, "POWERSAVE_MATCH"},
+            {HAPPY_BOX_MATCH, "HAPPY_BOX_MATCH"},
+            {RESTRICTED_MATCH, "RESTRICTED_MATCH"},
+            {LOW_POWER_STANDBY_MATCH, "LOW_POWER_STANDBY_MATCH"},
+            {IIF_MATCH, "IIF_MATCH"},
+            {LOCKDOWN_VPN_MATCH, "LOCKDOWN_VPN_MATCH"},
+            {OEM_DENY_1_MATCH, "OEM_DENY_1_MATCH"},
+            {OEM_DENY_2_MATCH, "OEM_DENY_2_MATCH"},
+            {OEM_DENY_3_MATCH, "OEM_DENY_3_MATCH"},
+            // clang-format on
+    };
+
+    for (const auto& config : testConfigs) {
+        SCOPED_TRACE(fmt::format("testConfig: [{}, {}]", config.uidOwnerMatchType,
+                     config.expected));
+
+        // Test private function uidMatchTypeToString() via dumpsys.
+        ASSERT_TRUE(isOk(updateUidOwnerMaps({TEST_UID}, config.uidOwnerMatchType,
+                                            TrafficController::IptOpInsert)));
+        std::vector<std::string> expectedLines;
+        expectedLines.emplace_back(fmt::format("{}  {}", TEST_UID, config.expected));
+        EXPECT_TRUE(expectDumpsysContains(expectedLines));
+
+        // Clean up the stubs.
+        ASSERT_TRUE(isOk(updateUidOwnerMaps({TEST_UID}, config.uidOwnerMatchType,
+                                            TrafficController::IptOpDelete)));
+    }
+}
+
 TEST_F(TrafficControllerTest, getFirewallType) {
     static const struct TestConfig {
         ChildChain childChain;
@@ -818,6 +901,7 @@
             {LOW_POWER_STANDBY, ALLOWLIST},
             {OEM_DENY_1, DENYLIST},
             {OEM_DENY_2, DENYLIST},
+            {OEM_DENY_3, DENYLIST},
             {INVALID_CHAIN, DENYLIST},
             // clang-format on
     };
diff --git a/service/src/com/android/server/TestNetworkService.java b/service/src/com/android/server/TestNetworkService.java
index e12190c..1209579 100644
--- a/service/src/com/android/server/TestNetworkService.java
+++ b/service/src/com/android/server/TestNetworkService.java
@@ -50,6 +50,7 @@
 import com.android.net.module.util.NetdUtils;
 import com.android.net.module.util.NetworkStackConstants;
 
+import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.net.Inet4Address;
 import java.net.Inet6Address;
@@ -76,7 +77,11 @@
     @NonNull private final NetworkProvider mNetworkProvider;
 
     // Native method stubs
-    private static native int jniCreateTunTap(boolean isTun, @NonNull String iface);
+    private static native int nativeCreateTunTap(boolean isTun, boolean hasCarrier,
+            @NonNull String iface);
+
+    private static native void nativeSetTunTapCarrierEnabled(@NonNull String iface, int tunFd,
+            boolean enabled);
 
     @VisibleForTesting
     protected TestNetworkService(@NonNull Context context) {
@@ -114,7 +119,7 @@
      * interface.
      */
     @Override
-    public TestNetworkInterface createInterface(boolean isTun, boolean bringUp,
+    public TestNetworkInterface createInterface(boolean isTun, boolean hasCarrier, boolean bringUp,
             LinkAddress[] linkAddrs, @Nullable String iface) {
         enforceTestNetworkPermissions(mContext);
 
@@ -130,8 +135,8 @@
 
         final long token = Binder.clearCallingIdentity();
         try {
-            ParcelFileDescriptor tunIntf =
-                    ParcelFileDescriptor.adoptFd(jniCreateTunTap(isTun, interfaceName));
+            ParcelFileDescriptor tunIntf = ParcelFileDescriptor.adoptFd(
+                    nativeCreateTunTap(isTun, hasCarrier, interfaceName));
             for (LinkAddress addr : linkAddrs) {
                 mNetd.interfaceAddAddress(
                         interfaceName,
@@ -375,4 +380,20 @@
     public static void enforceTestNetworkPermissions(@NonNull Context context) {
         context.enforceCallingOrSelfPermission(PERMISSION_NAME, "TestNetworkService");
     }
+
+    /** Enable / disable TestNetworkInterface carrier */
+    @Override
+    public void setCarrierEnabled(@NonNull TestNetworkInterface iface, boolean enabled) {
+        enforceTestNetworkPermissions(mContext);
+        nativeSetTunTapCarrierEnabled(iface.getInterfaceName(), iface.getFileDescriptor().getFd(),
+                enabled);
+        // Explicitly close fd after use to prevent StrictMode from complaining.
+        // Also, explicitly referencing iface guarantees that the object is not garbage collected
+        // before nativeSetTunTapCarrierEnabled() executes.
+        try {
+            iface.getFileDescriptor().close();
+        } catch (IOException e) {
+            // if the close fails, there is not much that can be done -- move on.
+        }
+    }
 }
diff --git a/service/src/com/android/server/connectivity/ClatCoordinator.java b/service/src/com/android/server/connectivity/ClatCoordinator.java
index 8cefd47..498cf63 100644
--- a/service/src/com/android/server/connectivity/ClatCoordinator.java
+++ b/service/src/com/android/server/connectivity/ClatCoordinator.java
@@ -498,6 +498,31 @@
         }
     }
 
+    private void maybeCleanUp(ParcelFileDescriptor tunFd, ParcelFileDescriptor readSock6,
+            ParcelFileDescriptor writeSock6) {
+        if (tunFd != null) {
+            try {
+                tunFd.close();
+            } catch (IOException e) {
+                Log.e(TAG, "Fail to close tun file descriptor " + e);
+            }
+        }
+        if (readSock6 != null) {
+            try {
+                readSock6.close();
+            } catch (IOException e) {
+                Log.e(TAG, "Fail to close read socket " + e);
+            }
+        }
+        if (writeSock6 != null) {
+            try {
+                writeSock6.close();
+            } catch (IOException e) {
+                Log.e(TAG, "Fail to close write socket " + e);
+            }
+        }
+    }
+
     /**
      * Start clatd for a given interface and NAT64 prefix.
      */
@@ -546,8 +571,15 @@
 
         // [3] Open, configure and bring up the tun interface.
         // Create the v4-... tun interface.
+
+        // Initialize all required file descriptors with null pointer. This makes the following
+        // error handling easier. Simply always call #maybeCleanUp for closing file descriptors,
+        // if any valid ones, in error handling.
+        ParcelFileDescriptor tunFd = null;
+        ParcelFileDescriptor readSock6 = null;
+        ParcelFileDescriptor writeSock6 = null;
+
         final String tunIface = CLAT_PREFIX + iface;
-        final ParcelFileDescriptor tunFd;
         try {
             tunFd = mDeps.adoptFd(mDeps.createTunInterface(tunIface));
         } catch (IOException e) {
@@ -556,7 +588,7 @@
 
         final int tunIfIndex = mDeps.getInterfaceIndex(tunIface);
         if (tunIfIndex == INVALID_IFINDEX) {
-            tunFd.close();
+            maybeCleanUp(tunFd, readSock6, writeSock6);
             throw new IOException("Fail to get interface index for interface " + tunIface);
         }
 
@@ -564,7 +596,6 @@
         try {
             mNetd.interfaceSetEnableIPv6(tunIface, false /* enabled */);
         } catch (RemoteException | ServiceSpecificException e) {
-            tunFd.close();
             Log.e(TAG, "Disable IPv6 on " + tunIface + " failed: " + e);
         }
 
@@ -575,19 +606,17 @@
             detectedMtu = mDeps.detectMtu(pfx96Str,
                 ByteBuffer.wrap(GOOGLE_DNS_4.getAddress()).getInt(), fwmark);
         } catch (IOException e) {
-            tunFd.close();
+            maybeCleanUp(tunFd, readSock6, writeSock6);
             throw new IOException("Detect MTU on " + tunIface + " failed: " + e);
         }
         final int mtu = adjustMtu(detectedMtu);
         Log.i(TAG, "ipv4 mtu is " + mtu);
 
-        // TODO: add setIptablesDropRule
-
         // Config tun interface mtu, address and bring up.
         try {
             mNetd.interfaceSetMtu(tunIface, mtu);
         } catch (RemoteException | ServiceSpecificException e) {
-            tunFd.close();
+            maybeCleanUp(tunFd, readSock6, writeSock6);
             throw new IOException("Set MTU " + mtu + " on " + tunIface + " failed: " + e);
         }
         final InterfaceConfigurationParcel ifConfig = new InterfaceConfigurationParcel();
@@ -599,14 +628,13 @@
         try {
             mNetd.interfaceSetCfg(ifConfig);
         } catch (RemoteException | ServiceSpecificException e) {
-            tunFd.close();
+            maybeCleanUp(tunFd, readSock6, writeSock6);
             throw new IOException("Setting IPv4 address to " + ifConfig.ipv4Addr + "/"
                     + ifConfig.prefixLength + " failed on " + ifConfig.ifName + ": " + e);
         }
 
         // [4] Open and configure local 464xlat read/write sockets.
         // Opens a packet socket to receive IPv6 packets in clatd.
-        final ParcelFileDescriptor readSock6;
         try {
             // Use a JNI call to get native file descriptor instead of Os.socket() because we would
             // like to use ParcelFileDescriptor to manage file descriptor. But ctor
@@ -614,27 +642,23 @@
             // descriptor to initialize ParcelFileDescriptor object instead.
             readSock6 = mDeps.adoptFd(mDeps.openPacketSocket());
         } catch (IOException e) {
-            tunFd.close();
+            maybeCleanUp(tunFd, readSock6, writeSock6);
             throw new IOException("Open packet socket failed: " + e);
         }
 
         // Opens a raw socket with a given fwmark to send IPv6 packets in clatd.
-        final ParcelFileDescriptor writeSock6;
         try {
             // Use a JNI call to get native file descriptor instead of Os.socket(). See above
             // reason why we use jniOpenPacketSocket6().
             writeSock6 = mDeps.adoptFd(mDeps.openRawSocket6(fwmark));
         } catch (IOException e) {
-            tunFd.close();
-            readSock6.close();
+            maybeCleanUp(tunFd, readSock6, writeSock6);
             throw new IOException("Open raw socket failed: " + e);
         }
 
         final int ifIndex = mDeps.getInterfaceIndex(iface);
         if (ifIndex == INVALID_IFINDEX) {
-            tunFd.close();
-            readSock6.close();
-            writeSock6.close();
+            maybeCleanUp(tunFd, readSock6, writeSock6);
             throw new IOException("Fail to get interface index for interface " + iface);
         }
 
@@ -642,9 +666,7 @@
         try {
             mDeps.addAnycastSetsockopt(writeSock6.getFileDescriptor(), v6Str, ifIndex);
         } catch (IOException e) {
-            tunFd.close();
-            readSock6.close();
-            writeSock6.close();
+            maybeCleanUp(tunFd, readSock6, writeSock6);
             throw new IOException("add anycast sockopt failed: " + e);
         }
 
@@ -653,9 +675,7 @@
         try {
             cookie = mDeps.tagSocketAsClat(writeSock6.getFileDescriptor());
         } catch (IOException e) {
-            tunFd.close();
-            readSock6.close();
-            writeSock6.close();
+            maybeCleanUp(tunFd, readSock6, writeSock6);
             throw new IOException("tag raw socket failed: " + e);
         }
 
@@ -663,9 +683,7 @@
         try {
             mDeps.configurePacketSocket(readSock6.getFileDescriptor(), v6Str, ifIndex);
         } catch (IOException e) {
-            tunFd.close();
-            readSock6.close();
-            writeSock6.close();
+            maybeCleanUp(tunFd, readSock6, writeSock6);
             throw new IOException("configure packet socket failed: " + e);
         }
 
@@ -679,9 +697,9 @@
             mDeps.untagSocket(cookie);
             throw new IOException("Error start clatd on " + iface + ": " + e);
         } finally {
-            tunFd.close();
-            readSock6.close();
-            writeSock6.close();
+            // The file descriptors have been duplicated (dup2) to clatd in native_startClatd().
+            // Close these file descriptor stubs which are unused anymore.
+            maybeCleanUp(tunFd, readSock6, writeSock6);
         }
 
         // [6] Initialize and store clatd tracker object.
diff --git a/tests/common/java/android/net/LinkPropertiesTest.java b/tests/common/java/android/net/LinkPropertiesTest.java
index 581ee22..9ed2bb3 100644
--- a/tests/common/java/android/net/LinkPropertiesTest.java
+++ b/tests/common/java/android/net/LinkPropertiesTest.java
@@ -20,7 +20,6 @@
 import static android.net.RouteInfo.RTN_UNICAST;
 import static android.net.RouteInfo.RTN_UNREACHABLE;
 
-import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
 import static com.android.testutils.ParcelUtils.parcelingRoundTrip;
 
@@ -53,6 +52,7 @@
 
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.RuleChain;
 import org.junit.runner.RunWith;
 
 import java.net.Inet4Address;
@@ -68,11 +68,13 @@
 @SmallTest
 @ConnectivityModuleTest
 public class LinkPropertiesTest {
+    // Use a RuleChain to explicitly specify the order of rules. DevSdkIgnoreRule must run before
+    // PlatformCompatChange rule, because otherwise tests with that should be skipped when targeting
+    // target SDK 33 will still attempt to override compat changes (which on user builds will crash)
+    // before being skipped.
     @Rule
-    public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
-
-    @Rule
-    public final PlatformCompatChangeRule compatChangeRule = new PlatformCompatChangeRule();
+    public final RuleChain chain = RuleChain.outerRule(
+            new DevSdkIgnoreRule()).around(new PlatformCompatChangeRule());
 
     private static final InetAddress ADDRV4 = address("75.208.6.1");
     private static final InetAddress ADDRV6 = address("2001:0db8:85a3:0000:0000:8a2e:0370:7334");
@@ -1262,7 +1264,8 @@
         assertFalse(lp.hasIpv4UnreachableDefaultRoute());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @CtsNetTestCasesMaxTargetSdk31(reason = "Compat change cannot be overridden when targeting T+")
     @EnableCompatChanges({LinkProperties.EXCLUDED_ROUTES})
     public void testHasExcludeRoute() {
         LinkProperties lp = new LinkProperties();
@@ -1274,7 +1277,8 @@
         assertTrue(lp.hasExcludeRoute());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @CtsNetTestCasesMaxTargetSdk31(reason = "Compat change cannot be overridden when targeting T+")
     @EnableCompatChanges({LinkProperties.EXCLUDED_ROUTES})
     public void testRouteAddWithSameKey() throws Exception {
         LinkProperties lp = new LinkProperties();
@@ -1291,7 +1295,8 @@
         assertEquals(2, lp.getRoutes().size());
     }
 
-    @Test @IgnoreUpTo(SC_V2)
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @CtsNetTestCasesMaxTargetSdk31(reason = "Compat change cannot be overridden when targeting T+")
     @EnableCompatChanges({LinkProperties.EXCLUDED_ROUTES})
     public void testExcludedRoutesEnabled() {
         final LinkProperties lp = new LinkProperties();
@@ -1307,8 +1312,8 @@
         assertEquals(3, lp.getRoutes().size());
     }
 
-    @Test @IgnoreUpTo(SC_V2)
-    @CtsNetTestCasesMaxTargetSdk31(reason = "Compat change cannot be overridden on T or above")
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @CtsNetTestCasesMaxTargetSdk31(reason = "Compat change cannot be overridden when targeting T+")
     @DisableCompatChanges({LinkProperties.EXCLUDED_ROUTES})
     public void testExcludedRoutesDisabled() {
         final LinkProperties lp = new LinkProperties();
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java
index 098f295..10775d0 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java
@@ -75,6 +75,8 @@
     @RequiredProperties({DOZE_MODE})
     public void testStartActivity_doze() throws Exception {
         setDozeMode(true);
+        // TODO (235284115): We need to turn on Doze every time before starting
+        // the activity.
         assertLaunchedActivityHasNetworkAccess("testStartActivity_doze");
     }
 
@@ -83,6 +85,8 @@
     public void testStartActivity_appStandby() throws Exception {
         turnBatteryOn();
         setAppIdle(true);
+        // TODO (235284115): We need to put the app into app standby mode every
+        // time before starting the activity.
         assertLaunchedActivityHasNetworkAccess("testStartActivity_appStandby");
     }
 
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 dc67c70..dd8b523 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
@@ -378,10 +378,6 @@
         if (mNetwork == null) {
             fail("VPN did not become available after " + TIMEOUT_MS + "ms");
         }
-
-        // Unfortunately, when the available callback fires, the VPN UID ranges are not yet
-        // configured. Give the system some time to do so. http://b/18436087 .
-        try { Thread.sleep(3000); } catch(InterruptedException e) {}
     }
 
     private void stopVpn() {
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java b/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java
index 3387fd7..cfd3130 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java
@@ -16,6 +16,8 @@
 
 package com.android.cts.net;
 
+import android.platform.test.annotations.FlakyTest;
+
 public class HostsideConnOnActivityStartTest extends HostsideNetworkTestCase {
     private static final String TEST_CLASS = TEST_PKG + ".ConnOnActivityStartTest";
     @Override
@@ -41,6 +43,7 @@
         runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_dataSaver");
     }
 
+    @FlakyTest(bugId = 231440256)
     public void testStartActivity_doze() throws Exception {
         runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_doze");
     }
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideLinkPropertiesGatingTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideLinkPropertiesGatingTests.java
index b65fb6b..9a1fa42 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideLinkPropertiesGatingTests.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideLinkPropertiesGatingTests.java
@@ -22,6 +22,9 @@
 
 /**
  * Tests for the {@link android.net.LinkProperties#EXCLUDED_ROUTES} compatibility change.
+ *
+ * TODO: see if we can delete this cumbersome host test by moving the coverage to CtsNetTestCases
+ * and CtsNetTestCasesMaxTargetSdk31.
  */
 public class HostsideLinkPropertiesGatingTests extends CompatChangeGatingTestCase {
     private static final String TEST_APK = "CtsHostsideNetworkTestsApp3.apk";
@@ -45,8 +48,19 @@
         runDeviceCompatTest("testExcludedRoutesChangeDisabled");
     }
 
-    public void testExcludedRoutesChangeDisabledByOverride() throws Exception {
+    public void testExcludedRoutesChangeDisabledByOverrideOnDebugBuild() throws Exception {
+        // Must install APK even when skipping test, because tearDown expects uninstall to succeed.
         installPackage(TEST_APK, true);
+
+        // This test uses an app with a target SDK where the compat change is on by default.
+        // Because user builds do not allow overriding compat changes, only run this test on debug
+        // builds. This seems better than deleting this test and not running it anywhere because we
+        // could in the future run this test on userdebug builds in presubmit.
+        //
+        // We cannot use assumeXyz here because CompatChangeGatingTestCase ultimately inherits from
+        // junit.framework.TestCase, which does not understand assumption failures.
+        if ("user".equals(getDevice().getProperty("ro.build.type"))) return;
+
         runDeviceCompatTestWithChangeDisabled("testExcludedRoutesChangeDisabled");
     }
 
diff --git a/tests/cts/net/src/android/net/cts/RateLimitTest.java b/tests/cts/net/src/android/net/cts/RateLimitTest.java
index 423f213..28cec1a 100644
--- a/tests/cts/net/src/android/net/cts/RateLimitTest.java
+++ b/tests/cts/net/src/android/net/cts/RateLimitTest.java
@@ -304,7 +304,7 @@
         // If this value is too low, this test might become flaky because of the burst value that
         // allows to send at a higher data rate for a short period of time. The faster the data rate
         // and the longer the test, the less this test will be affected.
-        final long dataLimitInBytesPerSecond = 1_000_000; // 1MB/s
+        final long dataLimitInBytesPerSecond = 2_000_000; // 2MB/s
         long resultInBytesPerSecond = runIngressDataRateMeasurement(Duration.ofSeconds(1));
         assertGreaterThan("Failed initial test with rate limit disabled", resultInBytesPerSecond,
                 dataLimitInBytesPerSecond);
@@ -315,9 +315,9 @@
         waitForTcPoliceFilterInstalled(Duration.ofSeconds(1));
 
         resultInBytesPerSecond = runIngressDataRateMeasurement(Duration.ofSeconds(10));
-        // Add 1% tolerance to reduce test flakiness. Burst size is constant at 128KiB.
+        // Add 10% tolerance to reduce test flakiness. Burst size is constant at 128KiB.
         assertLessThan("Failed test with rate limit enabled", resultInBytesPerSecond,
-                (long) (dataLimitInBytesPerSecond * 1.01));
+                (long) (dataLimitInBytesPerSecond * 1.1));
 
         ConnectivitySettingsManager.setIngressRateLimitInBytesPerSecond(mContext, -1);
 
diff --git a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
index 6dfadc7..bd1b74a 100644
--- a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
+++ b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
@@ -26,13 +26,8 @@
 import static android.net.TetheringManager.TETHER_ERROR_ENTITLEMENT_UNKNOWN;
 import static android.net.TetheringManager.TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION;
 import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
-import static android.net.TetheringManager.TETHER_ERROR_PROVISIONING_FAILED;
 import static android.net.cts.util.CtsTetheringUtils.isAnyIfaceMatch;
 
-import static com.android.networkstack.apishim.ConstantsShim.KEY_CARRIER_SUPPORTS_TETHERING_BOOL;
-import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
-import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -73,19 +68,14 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.TestCarrierConfigReceiver;
-
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
-import java.util.Objects;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
@@ -94,9 +84,6 @@
 @RunWith(AndroidJUnit4.class)
 public class TetheringManagerTest {
 
-    @Rule
-    public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
-
     private Context mContext;
 
     private ConnectivityManager mCm;
@@ -405,7 +392,7 @@
         // Override carrier config to ignore entitlement check.
         final PersistableBundle bundle = new PersistableBundle();
         bundle.putBoolean(CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL, false);
-        overrideCarrierConfig(bundle, CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL);
+        overrideCarrierConfig(bundle);
 
         // Verify that requestLatestTetheringEntitlementResult() can get entitlement
         // result TETHER_ERROR_NO_ERROR due to provisioning bypassed.
@@ -413,112 +400,14 @@
                 TETHERING_WIFI, false, c -> c.run(), listener), TETHER_ERROR_NO_ERROR);
 
         // Reset carrier config.
-        overrideCarrierConfig(null, "");
+        overrideCarrierConfig(null);
     }
 
-    @Test
-    @IgnoreUpTo(SC_V2)
-    public void testEnableTethering_carrierUnsupported_noTetheringActive() throws Exception {
-        assumeTrue(mPm.hasSystemFeature(FEATURE_TELEPHONY));
-
-        final TestTetheringEventCallback tetherEventCallback =
-                mCtsTetheringUtils.registerTetheringEventCallback();
-        boolean previousWifiEnabledState = false;
-        try {
-            tetherEventCallback.assumeWifiTetheringSupported(mContext);
-            // Avoid device connected to Wifi network.
-            previousWifiEnabledState = ensureCurrentNetworkIsCellular();
-            final PersistableBundle bundle = new PersistableBundle();
-            bundle.putBoolean(KEY_CARRIER_SUPPORTS_TETHERING_BOOL, false);
-            // Override carrier config to make carrier not support.
-            overrideCarrierConfig(bundle, KEY_CARRIER_SUPPORTS_TETHERING_BOOL);
-
-            mCtsTetheringUtils.startWifiTethering(tetherEventCallback);
-
-            mCtsTetheringUtils.expectSoftApDisabled();
-            tetherEventCallback.expectNoTetheringActive();
-        } finally {
-            overrideCarrierConfig(null, "");
-            mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
-            if (previousWifiEnabledState) {
-                mCtsNetUtils.connectToWifi();
-            }
-        }
-    }
-
-    @Test
-    @IgnoreUpTo(SC_V2)
-    public void testEnableTethering_carrierUnsupportByConfigChange_noTetheringActive()
-            throws Exception {
-        assumeTrue(mPm.hasSystemFeature(FEATURE_TELEPHONY));
-
-        final TestTetheringEventCallback tetherEventCallback =
-                mCtsTetheringUtils.registerTetheringEventCallback();
-        boolean previousWifiEnabledState = false;
-        try {
-            tetherEventCallback.assumeWifiTetheringSupported(mContext);
-            // Avoid device connected to Wifi network.
-            previousWifiEnabledState = ensureCurrentNetworkIsCellular();
-            mCtsTetheringUtils.startWifiTethering(tetherEventCallback);
-
-            final PersistableBundle bundle = new PersistableBundle();
-            bundle.putBoolean(KEY_CARRIER_SUPPORTS_TETHERING_BOOL, false);
-            // Override carrier config to make carrier not support.
-            overrideCarrierConfig(bundle, KEY_CARRIER_SUPPORTS_TETHERING_BOOL);
-
-            mCtsTetheringUtils.expectSoftApDisabled();
-            tetherEventCallback.expectNoTetheringActive();
-        } finally {
-            overrideCarrierConfig(null, "");
-            mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
-            if (previousWifiEnabledState) {
-                mCtsNetUtils.connectToWifi();
-            }
-        }
-    }
-
-    @Test
-    @IgnoreUpTo(SC_V2)
-    public void testRequestLatestEntitlementResult_carrierUnsupported() throws Exception {
-        assumeTrue(mTM.isTetheringSupported());
-        assumeTrue(mPm.hasSystemFeature(FEATURE_TELEPHONY));
-
-        final PersistableBundle bundle = new PersistableBundle();
-        bundle.putBoolean(KEY_CARRIER_SUPPORTS_TETHERING_BOOL, false);
-        try {
-            // Override carrier config to make carrier not support.
-            overrideCarrierConfig(bundle, KEY_CARRIER_SUPPORTS_TETHERING_BOOL);
-
-            // Verify that requestLatestTetheringEntitlementResult() can get entitlement
-            // result TETHER_ERROR_PROVISIONING_FAILED due to carrier unsupported
-            assertEntitlementResult(listener -> mTM.requestLatestTetheringEntitlementResult(
-                    TETHERING_WIFI,
-                    false,
-                    c -> c.run(),
-                    listener),
-                    TETHER_ERROR_PROVISIONING_FAILED);
-        } finally {
-            // Reset carrier config.
-            overrideCarrierConfig(null, "");
-        }
-    }
-
-    private void overrideCarrierConfig(PersistableBundle bundle, String configName)
-            throws Exception {
-        final int timeoutMs = 5_000;
-        final int currentSubId = SubscriptionManager.getDefaultSubscriptionId();
-        TestCarrierConfigReceiver configListener =
-                new TestCarrierConfigReceiver(mContext, currentSubId, timeoutMs, bundle,
-                        (configs) -> {
-                            if (bundle == null) {
-                                // This is to restore carrier config and means no need to do match.
-                                return true;
-                            }
-                            boolean requestConfigValue = bundle.getBoolean(configName);
-                            boolean receiveConfigValue = configs.getBoolean(configName);
-                            return Objects.equals(receiveConfigValue, requestConfigValue);
-                        });
-        configListener.overrideCarrierConfigForTest();
+    private void overrideCarrierConfig(PersistableBundle bundle) {
+        final CarrierConfigManager configManager = (CarrierConfigManager) mContext
+                .getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        final int subId = SubscriptionManager.getDefaultSubscriptionId();
+        configManager.overrideConfig(subId, bundle);
     }
 
     @Test
@@ -532,8 +421,30 @@
         try {
             tetherEventCallback.assumeWifiTetheringSupported(mContext);
             tetherEventCallback.expectNoTetheringActive();
-            // Avoid device connected to Wifi network.
-            previousWifiEnabledState = ensureCurrentNetworkIsCellular();
+
+            previousWifiEnabledState = mWm.isWifiEnabled();
+            if (previousWifiEnabledState) {
+                mCtsNetUtils.ensureWifiDisconnected(null);
+            }
+
+            final TestNetworkCallback networkCallback = new TestNetworkCallback();
+            Network activeNetwork = null;
+            try {
+                mCm.registerDefaultNetworkCallback(networkCallback);
+                activeNetwork = networkCallback.waitForAvailable();
+            } finally {
+                mCm.unregisterNetworkCallback(networkCallback);
+            }
+
+            assertNotNull("No active network. Please ensure the device has working mobile data.",
+                    activeNetwork);
+            final NetworkCapabilities activeNetCap = mCm.getNetworkCapabilities(activeNetwork);
+
+            // If active nework is ETHERNET, tethering may not use cell network as upstream.
+            assumeFalse(activeNetCap.hasTransport(TRANSPORT_ETHERNET));
+
+            assertTrue(activeNetCap.hasTransport(TRANSPORT_CELLULAR));
+
             mCtsTetheringUtils.startWifiTethering(tetherEventCallback);
 
             final TelephonyManager telephonyManager = (TelephonyManager) mContext.getSystemService(
@@ -553,36 +464,4 @@
             }
         }
     }
-
-    /**
-     * Make sure current network is cellular data.
-     * @return true Previous Wifi state is enabled, false is disabled.
-     */
-    private boolean ensureCurrentNetworkIsCellular() throws Exception {
-        final boolean previousWifiEnabledState = mWm.isWifiEnabled();
-        if (previousWifiEnabledState) {
-            mCtsNetUtils.ensureWifiDisconnected(null);
-        }
-
-        final TestNetworkCallback networkCallback = new TestNetworkCallback();
-        Network activeNetwork = null;
-        try {
-            mCm.registerDefaultNetworkCallback(networkCallback);
-            activeNetwork = networkCallback.waitForAvailable();
-        } finally {
-            mCm.unregisterNetworkCallback(networkCallback);
-        }
-
-        assertNotNull("No active network. Please ensure the device has working mobile data.",
-                activeNetwork);
-
-        final NetworkCapabilities activeNetCap = mCm.getNetworkCapabilities(activeNetwork);
-
-        // If active nework is ETHERNET, tethering may not use cell network as upstream.
-        assumeFalse(activeNetCap.hasTransport(TRANSPORT_ETHERNET));
-
-        assertTrue(activeNetCap.hasTransport(TRANSPORT_CELLULAR));
-
-        return previousWifiEnabledState;
-    }
 }
diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp
index 97c1265..b3684ac 100644
--- a/tests/integration/Android.bp
+++ b/tests/integration/Android.bp
@@ -71,7 +71,7 @@
         "net-tests-utils",
     ],
     libs: [
-        "service-connectivity",
+        "service-connectivity-for-tests",
         "services.core",
         "services.net",
     ],
diff --git a/tests/smoketest/Android.bp b/tests/smoketest/Android.bp
index df8ab74..4ab24fc 100644
--- a/tests/smoketest/Android.bp
+++ b/tests/smoketest/Android.bp
@@ -22,6 +22,6 @@
     static_libs: [
         "androidx.test.rules",
         "mockito-target-minus-junit4",
-        "service-connectivity",
+        "service-connectivity-for-tests",
     ],
 }
diff --git a/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
index 3047a16..8dfe924 100644
--- a/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
@@ -37,6 +37,7 @@
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 
@@ -556,14 +557,28 @@
                 () -> coordinator.clatStart(BASE_IFACE, NETID, NAT64_IP_PREFIX));
     }
 
-    private void checkNotStartClat(final TestDependencies deps, final boolean verifyTunFd,
-            final boolean verifyPacketSockFd, final boolean verifyRawSockFd) throws Exception {
+    private void checkNotStartClat(final TestDependencies deps, final boolean needToCloseTunFd,
+            final boolean needToClosePacketSockFd, final boolean needToCloseRawSockFd)
+            throws Exception {
         // [1] Expect that modified TestDependencies can't start clatd.
+        // Use precise check to make sure that there is no unexpected file descriptor closing.
         clearInvocations(TUN_PFD, RAW_SOCK_PFD, PACKET_SOCK_PFD);
         assertNotStartClat(deps);
-        if (verifyTunFd) verify(TUN_PFD).close();
-        if (verifyPacketSockFd) verify(PACKET_SOCK_PFD).close();
-        if (verifyRawSockFd) verify(RAW_SOCK_PFD).close();
+        if (needToCloseTunFd) {
+            verify(TUN_PFD).close();
+        } else {
+            verify(TUN_PFD, never()).close();
+        }
+        if (needToClosePacketSockFd) {
+            verify(PACKET_SOCK_PFD).close();
+        } else {
+            verify(PACKET_SOCK_PFD, never()).close();
+        }
+        if (needToCloseRawSockFd) {
+            verify(RAW_SOCK_PFD).close();
+        } else {
+            verify(RAW_SOCK_PFD, never()).close();
+        }
 
         // [2] Expect that unmodified TestDependencies can start clatd.
         // Used to make sure that the above modified TestDependencies has really broken the
@@ -582,8 +597,8 @@
                 throw new IOException();
             }
         }
-        checkNotStartClat(new FailureDependencies(), false /* verifyTunFd */,
-                false /* verifyPacketSockFd */, false /* verifyRawSockFd */);
+        checkNotStartClat(new FailureDependencies(), false /* needToCloseTunFd */,
+                false /* needToClosePacketSockFd */, false /* needToCloseRawSockFd */);
     }
 
     @Test
@@ -595,8 +610,8 @@
                 throw new IOException();
             }
         }
-        checkNotStartClat(new FailureDependencies(), false /* verifyTunFd */,
-                false /* verifyPacketSockFd */, false /* verifyRawSockFd */);
+        checkNotStartClat(new FailureDependencies(), false /* needToCloseTunFd */,
+                false /* needToClosePacketSockFd */, false /* needToCloseRawSockFd */);
     }
 
     @Test
@@ -607,8 +622,8 @@
                 throw new IOException();
             }
         }
-        checkNotStartClat(new FailureDependencies(), false /* verifyTunFd */,
-                false /* verifyPacketSockFd */, false /* verifyRawSockFd */);
+        checkNotStartClat(new FailureDependencies(), false /* needToCloseTunFd */,
+                false /* needToClosePacketSockFd */, false /* needToCloseRawSockFd */);
     }
 
     @Test
@@ -620,8 +635,8 @@
                 throw new IOException();
             }
         }
-        checkNotStartClat(new FailureDependencies(), true /* verifyTunFd */,
-                false /* verifyPacketSockFd */, false /* verifyRawSockFd */);
+        checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+                false /* needToClosePacketSockFd */, false /* needToCloseRawSockFd */);
     }
 
     @Test
@@ -632,8 +647,8 @@
                 throw new IOException();
             }
         }
-        checkNotStartClat(new FailureDependencies(), true /* verifyTunFd */,
-                false /* verifyPacketSockFd */, false /* verifyRawSockFd */);
+        checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+                false /* needToClosePacketSockFd */, false /* needToCloseRawSockFd */);
     }
 
     @Test
@@ -644,8 +659,8 @@
                 throw new IOException();
             }
         }
-        checkNotStartClat(new FailureDependencies(), true /* verifyTunFd */,
-                true /* verifyPacketSockFd */, false /* verifyRawSockFd */);
+        checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+                true /* needToClosePacketSockFd */, false /* needToCloseRawSockFd */);
     }
 
     @Test
@@ -657,8 +672,8 @@
                 throw new IOException();
             }
         }
-        checkNotStartClat(new FailureDependencies(), true /* verifyTunFd */,
-                true /* verifyPacketSockFd */, true /* verifyRawSockFd */);
+        checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+                true /* needToClosePacketSockFd */, true /* needToCloseRawSockFd */);
     }
 
     @Test
@@ -669,8 +684,8 @@
                 throw new IOException();
             }
         }
-        checkNotStartClat(new FailureDependencies(), true /* verifyTunFd */,
-                true /* verifyPacketSockFd */, true /* verifyRawSockFd */);
+        checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+                true /* needToClosePacketSockFd */, true /* needToCloseRawSockFd */);
     }
 
     @Test
@@ -682,8 +697,8 @@
                 throw new IOException();
             }
         }
-        checkNotStartClat(new FailureDependencies(), true /* verifyTunFd */,
-                true /* verifyPacketSockFd */, true /* verifyRawSockFd */);
+        checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+                true /* needToClosePacketSockFd */, true /* needToCloseRawSockFd */);
     }
 
     @Test
@@ -697,7 +712,7 @@
                 throw new IOException();
             }
         }
-        checkNotStartClat(new FailureDependencies(), true /* verifyTunFd */,
-                true /* verifyPacketSockFd */, true /* verifyRawSockFd */);
+        checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+                true /* needToClosePacketSockFd */, true /* needToCloseRawSockFd */);
     }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java b/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java
index ec51537..d1bf40e 100644
--- a/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java
@@ -27,6 +27,7 @@
 
 import static com.android.server.net.NetworkPolicyManagerInternal.QUOTA_TYPE_MULTIPATH;
 import static com.android.server.net.NetworkPolicyManagerService.OPPORTUNISTIC_QUOTA_UNKNOWN;
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
 import static org.junit.Assert.assertNotNull;
 import static org.mockito.ArgumentMatchers.any;
@@ -184,7 +185,7 @@
                 (int) setting);
     }
 
-    private void testGetMultipathPreference(
+    private void prepareGetMultipathPreferenceTest(
             long usedBytesToday, long subscriptionQuota, long policyWarning, long policyLimit,
             long defaultGlobalSetting, long defaultResSetting, boolean roaming) {
 
@@ -286,7 +287,7 @@
 
     @Test
     public void testGetMultipathPreference_SubscriptionQuota() {
-        testGetMultipathPreference(
+        prepareGetMultipathPreferenceTest(
                 DataUnit.MEGABYTES.toBytes(2) /* usedBytesToday */,
                 DataUnit.MEGABYTES.toBytes(14) /* subscriptionQuota */,
                 DataUnit.MEGABYTES.toBytes(100) /* policyWarning */,
@@ -301,7 +302,7 @@
 
     @Test
     public void testGetMultipathPreference_UserWarningQuota() {
-        testGetMultipathPreference(
+        prepareGetMultipathPreferenceTest(
                 DataUnit.MEGABYTES.toBytes(7) /* usedBytesToday */,
                 OPPORTUNISTIC_QUOTA_UNKNOWN,
                 // Remaining days are 29 days from Apr. 2nd to May 1st.
@@ -320,7 +321,7 @@
 
     @Test
     public void testGetMultipathPreference_SnoozedWarningQuota() {
-        testGetMultipathPreference(
+        prepareGetMultipathPreferenceTest(
                 DataUnit.MEGABYTES.toBytes(7) /* usedBytesToday */,
                 OPPORTUNISTIC_QUOTA_UNKNOWN,
                 POLICY_SNOOZED /* policyWarning */,
@@ -339,7 +340,7 @@
 
     @Test
     public void testGetMultipathPreference_SnoozedBothQuota() {
-        testGetMultipathPreference(
+        prepareGetMultipathPreferenceTest(
                 DataUnit.MEGABYTES.toBytes(7) /* usedBytesToday */,
                 OPPORTUNISTIC_QUOTA_UNKNOWN,
                 // 29 days from Apr. 2nd to May 1st
@@ -356,7 +357,7 @@
 
     @Test
     public void testGetMultipathPreference_SettingChanged() {
-        testGetMultipathPreference(
+        prepareGetMultipathPreferenceTest(
                 DataUnit.MEGABYTES.toBytes(2) /* usedBytesToday */,
                 OPPORTUNISTIC_QUOTA_UNKNOWN,
                 WARNING_DISABLED,
@@ -381,7 +382,7 @@
 
     @Test
     public void testGetMultipathPreference_ResourceChanged() {
-        testGetMultipathPreference(
+        prepareGetMultipathPreferenceTest(
                 DataUnit.MEGABYTES.toBytes(2) /* usedBytesToday */,
                 OPPORTUNISTIC_QUOTA_UNKNOWN,
                 WARNING_DISABLED,
@@ -404,4 +405,45 @@
         verify(mStatsManager, times(1)).registerUsageCallback(
                 any(), eq(DataUnit.MEGABYTES.toBytes(14)), any(), any());
     }
+
+    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+    @Test
+    public void testOnThresholdReached() {
+        prepareGetMultipathPreferenceTest(
+                DataUnit.MEGABYTES.toBytes(2) /* usedBytesToday */,
+                DataUnit.MEGABYTES.toBytes(14) /* subscriptionQuota */,
+                DataUnit.MEGABYTES.toBytes(100) /* policyWarning */,
+                LIMIT_DISABLED,
+                DataUnit.MEGABYTES.toBytes(12) /* defaultGlobalSetting */,
+                2_500_000 /* defaultResSetting */,
+                false /* roaming */);
+
+        final ArgumentCaptor<NetworkStatsManager.UsageCallback> usageCallbackCaptor =
+                ArgumentCaptor.forClass(NetworkStatsManager.UsageCallback.class);
+        final ArgumentCaptor<NetworkTemplate> networkTemplateCaptor =
+                ArgumentCaptor.forClass(NetworkTemplate.class);
+        // Verify the callback is registered with quota - used = 14 - 2 = 12MB.
+        verify(mStatsManager, times(1)).registerUsageCallback(
+                networkTemplateCaptor.capture(), eq(DataUnit.MEGABYTES.toBytes(12)), any(),
+                usageCallbackCaptor.capture());
+
+        // Capture arguments for later use.
+        final NetworkStatsManager.UsageCallback usageCallback = usageCallbackCaptor.getValue();
+        final NetworkTemplate template = networkTemplateCaptor.getValue();
+        assertNotNull(usageCallback);
+        assertNotNull(template);
+
+        // Decrease quota from 14 to 11, and trigger the event.
+        // TODO: Mock daily and monthly used bytes instead of changing subscription to simulate
+        //  remaining quota changed.
+        when(mNPMI.getSubscriptionOpportunisticQuota(TEST_NETWORK, QUOTA_TYPE_MULTIPATH))
+                .thenReturn(DataUnit.MEGABYTES.toBytes(11));
+        usageCallback.onThresholdReached(template);
+
+        // Callback must have been re-registered with new remaining quota = 11 - 2 = 9MB.
+        verify(mStatsManager, times(1))
+                .unregisterUsageCallback(eq(usageCallback));
+        verify(mStatsManager, times(1)).registerUsageCallback(
+                eq(template), eq(DataUnit.MEGABYTES.toBytes(9)), any(), eq(usageCallback));
+    }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index eb35469..cdfa190 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -1315,7 +1315,7 @@
                 config -> Arrays.asList(config.flags).contains(flag)));
     }
 
-    private void setupPlatformVpnWithSpecificExceptionAndItsErrorCode(IkeException exception,
+    private void doTestPlatformVpnWithException(IkeException exception,
             String category, int errorType, int errorCode) throws Exception {
         final ArgumentCaptor<IkeSessionCallback> captor =
                 ArgumentCaptor.forClass(IkeSessionCallback.class);
@@ -1333,6 +1333,7 @@
         // state
         verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS))
                 .createIkeSession(any(), any(), any(), any(), captor.capture(), any());
+        reset(mIkev2SessionCreator);
         final IkeSessionCallback ikeCb = captor.getValue();
         ikeCb.onClosedWithException(exception);
 
@@ -1342,6 +1343,23 @@
         if (errorType == VpnManager.ERROR_CLASS_NOT_RECOVERABLE) {
             verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS))
                     .unregisterNetworkCallback(eq(cb));
+        } else if (errorType == VpnManager.ERROR_CLASS_RECOVERABLE) {
+            // To prevent spending much time to test the retry function, only retry 2 times here.
+            int retryIndex = 0;
+            verify(mIkev2SessionCreator,
+                    timeout(((TestDeps) vpn.mDeps).getNextRetryDelaySeconds(retryIndex++) * 1000
+                            + TEST_TIMEOUT_MS))
+                    .createIkeSession(any(), any(), any(), any(), captor.capture(), any());
+
+            // Capture a new IkeSessionCallback to get the latest token.
+            reset(mIkev2SessionCreator);
+            final IkeSessionCallback ikeCb2 = captor.getValue();
+            ikeCb2.onClosedWithException(exception);
+            verify(mIkev2SessionCreator,
+                    timeout(((TestDeps) vpn.mDeps).getNextRetryDelaySeconds(retryIndex++) * 1000
+                            + TEST_TIMEOUT_MS))
+                    .createIkeSession(any(), any(), any(), any(), captor.capture(), any());
+            reset(mIkev2SessionCreator);
         }
     }
 
@@ -1350,7 +1368,7 @@
         final IkeProtocolException exception = mock(IkeProtocolException.class);
         final int errorCode = IkeProtocolException.ERROR_TYPE_AUTHENTICATION_FAILED;
         when(exception.getErrorType()).thenReturn(errorCode);
-        setupPlatformVpnWithSpecificExceptionAndItsErrorCode(exception,
+        doTestPlatformVpnWithException(exception,
                 VpnManager.CATEGORY_EVENT_IKE_ERROR, VpnManager.ERROR_CLASS_NOT_RECOVERABLE,
                 errorCode);
     }
@@ -1360,7 +1378,7 @@
         final IkeProtocolException exception = mock(IkeProtocolException.class);
         final int errorCode = IkeProtocolException.ERROR_TYPE_TEMPORARY_FAILURE;
         when(exception.getErrorType()).thenReturn(errorCode);
-        setupPlatformVpnWithSpecificExceptionAndItsErrorCode(exception,
+        doTestPlatformVpnWithException(exception,
                 VpnManager.CATEGORY_EVENT_IKE_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE, errorCode);
     }
 
@@ -1370,7 +1388,7 @@
         final UnknownHostException unknownHostException = new UnknownHostException();
         final int errorCode = VpnManager.ERROR_CODE_NETWORK_UNKNOWN_HOST;
         when(exception.getCause()).thenReturn(unknownHostException);
-        setupPlatformVpnWithSpecificExceptionAndItsErrorCode(exception,
+        doTestPlatformVpnWithException(exception,
                 VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
                 errorCode);
     }
@@ -1382,7 +1400,7 @@
                 new IkeTimeoutException("IkeTimeoutException");
         final int errorCode = VpnManager.ERROR_CODE_NETWORK_PROTOCOL_TIMEOUT;
         when(exception.getCause()).thenReturn(ikeTimeoutException);
-        setupPlatformVpnWithSpecificExceptionAndItsErrorCode(exception,
+        doTestPlatformVpnWithException(exception,
                 VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
                 errorCode);
     }
@@ -1391,7 +1409,7 @@
     public void testStartPlatformVpnFailedWithIkeNetworkLostException() throws Exception {
         final IkeNetworkLostException exception = new IkeNetworkLostException(
                 new Network(100));
-        setupPlatformVpnWithSpecificExceptionAndItsErrorCode(exception,
+        doTestPlatformVpnWithException(exception,
                 VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
                 VpnManager.ERROR_CODE_NETWORK_LOST);
     }
@@ -1402,7 +1420,7 @@
         final IOException ioException = new IOException();
         final int errorCode = VpnManager.ERROR_CODE_NETWORK_IO;
         when(exception.getCause()).thenReturn(ioException);
-        setupPlatformVpnWithSpecificExceptionAndItsErrorCode(exception,
+        doTestPlatformVpnWithException(exception,
                 VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
                 errorCode);
     }
@@ -1694,6 +1712,11 @@
         public DeviceIdleInternal getDeviceIdleInternal() {
             return mDeviceIdleInternal;
         }
+
+        public long getNextRetryDelaySeconds(int retryCount) {
+            // Simply return retryCount as the delay seconds for retrying.
+            return retryCount;
+        }
     }
 
     /**
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index f1820b3..4fbbc75 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -140,16 +140,6 @@
 import com.android.testutils.TestBpfMap;
 import com.android.testutils.TestableNetworkStatsProviderBinder;
 
-import java.io.File;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.time.Clock;
-import java.time.ZoneOffset;
-import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicBoolean;
-
 import libcore.testing.io.TestIoUtils;
 
 import org.junit.After;
@@ -161,6 +151,19 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Clock;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+
 /**
  * Tests for {@link NetworkStatsService}.
  *
@@ -1105,6 +1108,40 @@
     }
 
     @Test
+    public void testGetLatestSummary() throws Exception {
+        // Pretend that network comes online.
+        expectDefaultSettings();
+        NetworkStateSnapshot[] states = new NetworkStateSnapshot[]{buildWifiState()};
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+
+        mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        // Increase arbitrary time which does not align to the bucket edge, create some traffic.
+        incrementCurrentTime(1751000L);
+        NetworkStats.Entry entry = new NetworkStats.Entry(
+                TEST_IFACE, UID_ALL, SET_DEFAULT, TAG_NONE, 50L, 5L, 51L, 1L, 3L);
+        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1).insertEntry(entry));
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        forcePollAndWaitForIdle();
+
+        // Verify the mocked stats is returned by querying with the range of the latest bucket.
+        final ZonedDateTime end =
+                ZonedDateTime.ofInstant(mClock.instant(), ZoneId.systemDefault());
+        final ZonedDateTime start = end.truncatedTo(ChronoUnit.HOURS);
+        NetworkStats stats = mSession.getSummaryForNetwork(buildTemplateWifi(TEST_WIFI_NETWORK_KEY),
+                start.toInstant().toEpochMilli(), end.toInstant().toEpochMilli());
+        assertEquals(1, stats.size());
+        assertValues(stats, IFACE_ALL, UID_ALL, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 50L, 5L, 51L, 1L, 3L);
+
+        // For getHistoryIntervalForNetwork, only includes buckets that atomically occur in
+        // the inclusive time range, instead of including the latest bucket. This behavior is
+        // already documented publicly, refer to {@link NetworkStatsManager#queryDetails}.
+    }
+
+    @Test
     public void testUidStatsForTransport() throws Exception {
         // pretend that network comes online
         expectDefaultSettings();
@@ -1135,9 +1172,12 @@
 
         assertEquals(3, stats.size());
         entry1.operations = 1;
+        entry1.iface = null;
         assertEquals(entry1, stats.getValues(0, null));
         entry2.operations = 1;
+        entry2.iface = null;
         assertEquals(entry2, stats.getValues(1, null));
+        entry3.iface = null;
         assertEquals(entry3, stats.getValues(2, null));
     }