Merge "[Thread] add vendor & model name support for net diag" into main
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringService.java b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
index b92cf69..737041e 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringService.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
@@ -146,7 +146,7 @@
             mTethering.setUsbTethering(enable, listener);
         }
 
-        private boolean isRequestAllowedForDeviceOwner(@NonNull TetheringRequest request) {
+        private boolean isRequestAllowedForDOOrCarrierApp(@NonNull TetheringRequest request) {
             return request.getTetheringType() == TETHERING_WIFI
                     && request.getSoftApConfiguration() != null;
         }
@@ -159,10 +159,10 @@
             request.setPackageName(callerPkg);
             boolean onlyAllowPrivileged = request.isExemptFromEntitlementCheck()
                     || request.getInterfaceName() != null;
-            boolean isDeviceOwnerAllowed = mTethering.isTetheringWithSoftApConfigEnabled()
-                    && isRequestAllowedForDeviceOwner(request);
+            boolean isDOOrCarrierAppAllowed = mTethering.isTetheringWithSoftApConfigEnabled()
+                    && isRequestAllowedForDOOrCarrierApp(request);
             if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, onlyAllowPrivileged,
-                    isDeviceOwnerAllowed, listener)) {
+                    isDOOrCarrierAppAllowed, listener)) {
                 return;
             }
             mTethering.startTethering(request, callerPkg, listener);
@@ -191,10 +191,10 @@
             if (listener == null) return;
             request.setUid(getBinderCallingUid());
             request.setPackageName(callerPkg);
-            boolean isDeviceOwnerAllowed = mTethering.isTetheringWithSoftApConfigEnabled()
-                    && isRequestAllowedForDeviceOwner(request);
+            boolean isDOOrCarrierAppAllowed = mTethering.isTetheringWithSoftApConfigEnabled()
+                    && isRequestAllowedForDOOrCarrierApp(request);
             if (checkAndNotifyCommonError(callerPkg, callingAttributionTag,
-                    false /* onlyAllowPrivileged */, isDeviceOwnerAllowed, listener)) {
+                    false /* onlyAllowPrivileged */, isDOOrCarrierAppAllowed, listener)) {
                 return;
             }
             // Note: Whether tethering is actually stopped or not will depend on whether the request
@@ -274,9 +274,9 @@
         @Override
         public void isTetheringSupported(String callerPkg, String callingAttributionTag,
                 IIntResultListener listener) {
-            boolean isDeviceOwnerAppAllowed = mTethering.isTetheringWithSoftApConfigEnabled();
+            boolean isDOOrCarrierAppAllowed = mTethering.isTetheringWithSoftApConfigEnabled();
             if (checkAndNotifyCommonError(callerPkg, callingAttributionTag,
-                    false /* onlyAllowPrivileged */, isDeviceOwnerAppAllowed, listener)) {
+                    false /* onlyAllowPrivileged */, isDOOrCarrierAppAllowed, listener)) {
                 return;
             }
             try {
@@ -304,7 +304,7 @@
 
         private boolean checkAndNotifyCommonError(final String callerPkg,
                 final String callingAttributionTag, final boolean onlyAllowPrivileged,
-                final boolean isDeviceOwnerAppAllowed, final IIntResultListener listener) {
+                final boolean isDOOrCarrierAppAllowed, final IIntResultListener listener) {
             try {
                 final int uid = getBinderCallingUid();
                 if (!checkPackageNameMatchesUid(uid, callerPkg)) {
@@ -313,7 +313,7 @@
                     return true;
                 }
                 if (!hasTetherChangePermission(uid, callerPkg, callingAttributionTag,
-                        onlyAllowPrivileged, isDeviceOwnerAppAllowed)) {
+                        onlyAllowPrivileged, isDOOrCarrierAppAllowed)) {
                     listener.onResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
                     return true;
                 }
@@ -347,14 +347,18 @@
 
         private boolean hasTetherChangePermission(final int uid, final String callerPkg,
                 final String callingAttributionTag, final boolean onlyAllowPrivileged,
-                final boolean isDeviceOwnerAppAllowed) {
+                final boolean isDOOrCarrierAppAllowed) {
             if (onlyAllowPrivileged && !hasNetworkStackPermission()
                     && !hasNetworkSettingsPermission()) return false;
 
             if (hasTetherPrivilegedPermission()) return true;
 
-            // Allow DO apps to change tethering even if they don't have TETHER_PRIVILEGED.
-            if (isDeviceOwnerAppAllowed && mService.isDeviceOwner(uid, callerPkg)) {
+            // Allow DO and carrier-privileged apps to change tethering even if they don't have
+            // TETHER_PRIVILEGED.
+            // TODO: Stop tethering if the app loses DO status or carrier-privileges.
+            if (isDOOrCarrierAppAllowed
+                    && (mService.isDeviceOwner(uid, callerPkg)
+                            || mService.isCarrierPrivileged(callerPkg))) {
                 return true;
             }
 
@@ -436,6 +440,14 @@
     }
 
     /**
+     * Wrapper for {@link TetheringPermissionsUtils#isCarrierPrivileged(String)}, used for mocks.
+     */
+    @VisibleForTesting
+    boolean isCarrierPrivileged(final String callerPkg) {
+        return mTetheringPermissionsUtils.isCarrierPrivileged(callerPkg);
+    }
+
+    /**
      * An injection method for testing.
      */
     @VisibleForTesting
diff --git a/Tethering/src/com/android/networkstack/tethering/util/TetheringPermissionsUtils.java b/Tethering/src/com/android/networkstack/tethering/util/TetheringPermissionsUtils.java
index 944e861..603fa9c 100644
--- a/Tethering/src/com/android/networkstack/tethering/util/TetheringPermissionsUtils.java
+++ b/Tethering/src/com/android/networkstack/tethering/util/TetheringPermissionsUtils.java
@@ -19,7 +19,9 @@
 import android.app.admin.DevicePolicyManager;
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.os.Binder;
 import android.os.UserHandle;
+import android.telephony.TelephonyManager;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
@@ -68,4 +70,20 @@
         }
         return devicePolicyManager;
     }
+
+    /**
+     * Checks if the package name has carrier privileges.
+     */
+    public boolean isCarrierPrivileged(@NonNull final String packageName) {
+        TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class);
+        if (telephonyManager == null) return false;
+
+        long ident = Binder.clearCallingIdentity();
+        try {
+            return telephonyManager.checkCarrierPrivilegesForPackageAnyPhone(packageName)
+                    == TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS;
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
 }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/MockTetheringService.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/MockTetheringService.java
index a8bd221..01d7198 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/MockTetheringService.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/MockTetheringService.java
@@ -37,6 +37,7 @@
     private final ArrayMap<String, Integer> mMockedPermissions = new ArrayMap<>();
     private final ArrayMap<String, Integer> mMockedPackageUids = new ArrayMap<>();
     private final Set<String> mMockedDeviceOwnerPackages = new ArraySet<>();
+    private final Set<String> mMockedCarrierPrivilegedPackages = new ArraySet<>();
     private int mMockCallingUid;
 
     @Override
@@ -83,6 +84,11 @@
         return mMockedDeviceOwnerPackages.contains(callerPkg);
     }
 
+    @Override
+    boolean isCarrierPrivileged(final String callerPkg) {
+        return mMockedCarrierPrivilegedPackages.contains(callerPkg);
+    }
+
     public Tethering getTethering() {
         return mTethering;
     }
@@ -141,5 +147,19 @@
         public void removeDeviceOwnerPackage(final String packageName) {
             mMockedDeviceOwnerPackages.remove(packageName);
         }
+
+        /**
+         * Add a mocked carrier privileges package
+         */
+        public void addCarrierPrivilegedPackage(final String packageName) {
+            mMockedCarrierPrivilegedPackages.add(packageName);
+        }
+
+        /**
+         * Remove a mocked carrier privileges package
+         */
+        public void removeCarrierPrivilegedPackage(final String packageName) {
+            mMockedCarrierPrivilegedPackages.remove(packageName);
+        }
     }
 }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
index 87163ef..b58fa14 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
@@ -90,6 +90,15 @@
     private static final int TEST_CALLER_UID = 1234;
     private static final String TEST_ATTRIBUTION_TAG = null;
     private static final String TEST_WRONG_PACKAGE = "wrong.package";
+    private static final int NO_RESULT = -1;
+    private static final TetheringRequest USB_REQUEST =
+            new TetheringRequest.Builder(TETHERING_USB).build();
+    private static final TetheringRequest WIFI_REQUEST_NO_CONFIG =
+            new TetheringRequest.Builder(TETHERING_WIFI).build();
+    private static final TetheringRequest WIFI_REQUEST_WITH_CONFIG =
+            new TetheringRequest.Builder(TETHERING_WIFI)
+                    .setSoftApConfiguration(new SoftApConfiguration.Builder().build())
+                    .build();
     @Mock private ITetheringEventCallback mITetheringEventCallback;
     @Rule public ServiceTestRule mServiceTestRule;
     private Tethering mTethering;
@@ -100,7 +109,7 @@
     @Mock private AppOpsManager mAppOps;
 
     private class TestTetheringResult extends IIntResultListener.Stub {
-        private int mResult = -1; // Default value that does not match any result code.
+        private int mResult = NO_RESULT;
         @Override
         public void onResult(final int resultCode) {
             mResult = resultCode;
@@ -115,7 +124,7 @@
         MyResultReceiver(Handler handler) {
             super(handler);
         }
-        private int mResult = -1; // Default value that does not match any result code.
+        private int mResult = NO_RESULT;
         @Override
         protected void onReceiveResult(int resultCode, Bundle resultData) {
             mResult = resultCode;
@@ -206,6 +215,21 @@
         mMockConnector.removeDeviceOwnerPackage(TEST_CALLER_PKG);
     }
 
+    private void runAsCarrierPrivileged(final TestTetheringCall test) throws Exception {
+        mMockConnector.addCarrierPrivilegedPackage(TEST_CALLER_PKG);
+        runTetheringCall(test, true /* isTetheringAllowed */,
+                true /* isTetheringWithSoftApConfigEnabled */, new String[0]);
+        mMockConnector.removeCarrierPrivilegedPackage(TEST_CALLER_PKG);
+    }
+
+    private void runAsCarrierPrivilegedWhenCarrierPrivilegeBypassNotEnabled(
+            final TestTetheringCall test) throws Exception {
+        mMockConnector.addCarrierPrivilegedPackage(TEST_CALLER_PKG);
+        runTetheringCall(test, true /* isTetheringAllowed */,
+                false /* isTetheringWithSoftApConfigEnabled */, new String[0]);
+        mMockConnector.removeCarrierPrivilegedPackage(TEST_CALLER_PKG);
+    }
+
     private void runTetheringCall(final TestTetheringCall test, boolean isTetheringAllowed,
             boolean isTetheringWithSoftApConfigEnabled, String... permissions) throws Exception {
         // Allow the test to run even if ACCESS_NETWORK_STATE was granted at the APK level
@@ -381,125 +405,114 @@
         });
     }
 
-    private void runStartTethering(final TestTetheringResult result,
-            final TetheringRequestParcel request) throws Exception {
-        mTetheringConnector.startTethering(request, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
-                result);
-        verify(mTethering, atLeastOnce()).isTetheringWithSoftApConfigEnabled();
-        verify(mTethering).isTetheringSupported();
-        verify(mTethering).isTetheringAllowed();
-        verify(mTethering).startTethering(
-                eq(new TetheringRequest(request)), eq(TEST_CALLER_PKG), eq(result));
+    private void verifyStartTetheringRequestSucceeds(final TetheringRequest request,
+            final TestTetheringResult result) throws Exception {
+        mTetheringConnector.startTethering(request.getParcel(), TEST_CALLER_PKG,
+                TEST_ATTRIBUTION_TAG, result);
+        verify(mTethering).startTethering(eq(request), eq(TEST_CALLER_PKG), eq(result));
+        reset(mTethering);
+        result.assertResult(NO_RESULT);
+    }
+
+    private void verifyStartTetheringRequestFails(final TetheringRequest request,
+            final TestTetheringResult result, final int resultCode) throws Exception {
+        mTetheringConnector.startTethering(request.getParcel(), TEST_CALLER_PKG,
+                TEST_ATTRIBUTION_TAG, result);
+        verify(mTethering, never()).startTethering(any(), any(), any());
+        reset(mTethering);
+        result.assertResult(resultCode);
+    }
+
+    private void verifyStartTetheringRequestWithWrongPackageFails(final TetheringRequest request,
+            final TestTetheringResult result) throws Exception {
+        mTetheringConnector.startTethering(request.getParcel(), TEST_WRONG_PACKAGE,
+                TEST_ATTRIBUTION_TAG, result);
+        verify(mTethering, never()).startTethering(any(), any(), any());
+        reset(mTethering);
+        result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
     }
 
     @Test
     public void testStartTethering() throws Exception {
-        final TetheringRequestParcel request = new TetheringRequestParcel();
-        request.tetheringType = TETHERING_WIFI;
-
         runAsNoPermission((result) -> {
-            mTetheringConnector.startTethering(request, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
-                    result);
-            verify(mTethering, atLeastOnce()).isTetheringWithSoftApConfigEnabled();
-            result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
-            verifyNoMoreInteractionsForTethering();
-        });
-
-        // Not a Wifi request - Fail
-        runAsDeviceOwner((result) -> {
-            final TetheringRequestParcel notWifi = new TetheringRequestParcel();
-            notWifi.tetheringType = TETHERING_USB;
-            mTetheringConnector.startTethering(notWifi, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
-                    result);
-            verify(mTethering, atLeastOnce()).isTetheringWithSoftApConfigEnabled();
-            result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
-            verifyNoMoreInteractionsForTethering();
-        });
-
-        // Request has no SoftApConfiguration - Fail
-        runAsDeviceOwner((result) -> {
-            final TetheringRequestParcel noConfig = new TetheringRequestParcel();
-            noConfig.tetheringType = TETHERING_WIFI;
-            mTetheringConnector.startTethering(noConfig, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
-                    result);
-            verify(mTethering, atLeastOnce()).isTetheringWithSoftApConfigEnabled();
-            result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
-            verifyNoMoreInteractionsForTethering();
-        });
-
-        // Wifi request with SoftApConfiguration - Succeed
-        runAsDeviceOwner((result) -> {
-            final TetheringRequestParcel withConfig = new TetheringRequestParcel();
-            withConfig.tetheringType = TETHERING_WIFI;
-            withConfig.softApConfig = new SoftApConfiguration.Builder().build();
-            mTetheringConnector.startTethering(withConfig, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
-                    result);
-            verify(mTethering, atLeastOnce()).isTetheringWithSoftApConfigEnabled();
-            verify(mTethering).isTetheringSupported();
-            verify(mTethering).isTetheringAllowed();
-            verify(mTethering).startTethering(any(), any(), any());
-            result.assertResult(-1); // No result
-            verifyNoMoreInteractionsForTethering();
-        });
-
-        runAsDeviceOwnerWhenDeviceOwnerBypassNotEnabled((result) -> {
-            mTetheringConnector.startTethering(request, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
-                    result);
-            verify(mTethering, atLeastOnce()).isTetheringWithSoftApConfigEnabled();
-            result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
-            verify(mTethering).isTetherProvisioningRequired();
-            verifyNoMoreInteractionsForTethering();
+            verifyStartTetheringRequestFails(WIFI_REQUEST_NO_CONFIG, result,
+                    TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
         });
 
         runAsTetherPrivileged((result) -> {
-            mTetheringConnector.startTethering(request, TEST_WRONG_PACKAGE,
-                    TEST_ATTRIBUTION_TAG, result);
-            verify(mTethering, atLeastOnce()).isTetheringWithSoftApConfigEnabled();
-            verify(mTethering, never()).startTethering(
-                    eq(new TetheringRequest(request)), eq(TEST_WRONG_PACKAGE), eq(result));
-            result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
-            verifyNoMoreInteractionsForTethering();
+            verifyStartTetheringRequestSucceeds(WIFI_REQUEST_NO_CONFIG, result);
         });
 
         runAsTetherPrivileged((result) -> {
-            runStartTethering(result, request);
-            verifyNoMoreInteractionsForTethering();
+            verifyStartTetheringRequestWithWrongPackageFails(WIFI_REQUEST_NO_CONFIG, result);
         });
 
         runAsWriteSettings((result) -> {
-            mTetheringConnector.startTethering(request, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
-                    result);
-            verify(mTethering, atLeastOnce()).isTetheringWithSoftApConfigEnabled();
-            result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
-            verifyNoMoreInteractionsForTethering();
+            verifyStartTetheringRequestFails(WIFI_REQUEST_NO_CONFIG, result,
+                    TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
         });
 
         runAsWriteSettingsWhenWriteSettingsAllowed((result) -> {
-            runStartTethering(result, request);
-            verify(mTethering, atLeastOnce()).isTetheringWithSoftApConfigEnabled();
-            verify(mTethering).isTetherProvisioningRequired();
-            verifyNoMoreInteractionsForTethering();
+            verifyStartTetheringRequestSucceeds(WIFI_REQUEST_NO_CONFIG, result);
         });
 
         runAsTetheringDisallowed((result) -> {
-            mTetheringConnector.startTethering(request, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
-                    result);
-            verify(mTethering, atLeastOnce()).isTetheringWithSoftApConfigEnabled();
-            verify(mTethering).isTetheringSupported();
-            verify(mTethering).isTetheringAllowed();
-            result.assertResult(TETHER_ERROR_UNSUPPORTED);
-            verifyNoMoreInteractionsForTethering();
+            verifyStartTetheringRequestFails(WIFI_REQUEST_NO_CONFIG, result,
+                    TETHER_ERROR_UNSUPPORTED);
+        });
+
+        // Not wifi -> fail
+        runAsDeviceOwner((result) -> {
+            verifyStartTetheringRequestFails(USB_REQUEST, result,
+                    TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+        });
+
+        // No SoftApConfiguration -> fail
+        runAsDeviceOwner((result) -> {
+            verifyStartTetheringRequestFails(WIFI_REQUEST_NO_CONFIG, result,
+                    TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+        });
+
+        // With SoftApConfiguration -> success
+        runAsDeviceOwner((result) -> {
+            verifyStartTetheringRequestSucceeds(WIFI_REQUEST_WITH_CONFIG, result);
+        });
+
+        runAsDeviceOwnerWhenDeviceOwnerBypassNotEnabled((result) -> {
+            verifyStartTetheringRequestFails(WIFI_REQUEST_WITH_CONFIG, result,
+                    TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+        });
+
+        // Not wifi -> fail
+        runAsCarrierPrivileged((result) -> {
+            verifyStartTetheringRequestFails(USB_REQUEST, result,
+                    TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+        });
+
+        // No SoftApConfiguration -> fail
+        runAsCarrierPrivileged((result) -> {
+            verifyStartTetheringRequestFails(WIFI_REQUEST_NO_CONFIG, result,
+                    TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+        });
+
+        // With SoftApConfiguration -> success
+        runAsCarrierPrivileged((result) -> {
+            verifyStartTetheringRequestSucceeds(WIFI_REQUEST_WITH_CONFIG, result);
+        });
+
+        runAsCarrierPrivilegedWhenCarrierPrivilegeBypassNotEnabled((result) -> {
+            verifyStartTetheringRequestFails(WIFI_REQUEST_WITH_CONFIG, result,
+                    TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
         });
     }
 
     @Test
     public void testStartTetheringWithInterfaceSucceeds() throws Exception {
-        final TetheringRequestParcel request = new TetheringRequestParcel();
-        request.tetheringType = TETHERING_VIRTUAL;
-        request.interfaceName = "avf_tap_fixed";
-
+        final TetheringRequest request = new TetheringRequest.Builder(TETHERING_VIRTUAL)
+                .setInterfaceName("avf_tap_fixed")
+                .build();
         runAsNetworkSettings((result) -> {
-            runStartTethering(result, request);
+            verifyStartTetheringRequestSucceeds(request, result);
             verifyNoMoreInteractionsForTethering();
         });
     }
@@ -599,84 +612,110 @@
         });
     }
 
-    private void verifyHasPermissionForStopTetheringRequest(TetheringRequest request,
+    private void verifyStopTetheringRequestSucceeds(final TetheringRequest request,
             final TestTetheringResult result) throws Exception {
         mTetheringConnector.stopTetheringRequest(request, TEST_CALLER_PKG,
                 TEST_ATTRIBUTION_TAG, result);
         verify(mTethering).stopTetheringRequest(any(), any());
-        verify(mTethering).isTetheringSupported();
-        verify(mTethering).isTetheringAllowed();
         reset(mTethering);
+        result.assertResult(NO_RESULT);
     }
 
-    private void verifyDoesNotHavePermissionForStopTetheringRequest(TetheringRequest request,
-            final TestTetheringResult result) throws Exception {
+    private void verifyStopTetheringRequestFails(final TetheringRequest request,
+            final TestTetheringResult result, int resultCode) throws Exception {
         mTetheringConnector.stopTetheringRequest(request, TEST_CALLER_PKG,
                 TEST_ATTRIBUTION_TAG, result);
         verify(mTethering, never()).stopTetheringRequest(any(), any());
+        reset(mTethering);
+        result.assertResult(resultCode);
+    }
+
+    private void verifyStopTetheringRequestWithWrongPackageFails(final TetheringRequest request,
+            final TestTetheringResult result) throws Exception {
+        mTetheringConnector.stopTetheringRequest(request, TEST_WRONG_PACKAGE,
+                TEST_ATTRIBUTION_TAG, result);
+        verify(mTethering, never()).stopTetheringRequest(any(), any());
+        reset(mTethering);
         result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
-        reset(mTethering);
-    }
-
-    private void verifyStopTetheringRequestWithTetheringDisallowed(TetheringRequest request,
-            final TestTetheringResult result) throws Exception {
-        mTetheringConnector.stopTetheringRequest(request, TEST_CALLER_PKG,
-                TEST_ATTRIBUTION_TAG, result);
-        verify(mTethering, never()).stopTetheringRequest(any(), any());
-        result.assertResult(TETHER_ERROR_UNSUPPORTED);
-        reset(mTethering);
     }
 
     @Test
     public void testStopTetheringRequest() throws Exception {
-        TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build();
-
         runAsNoPermission((result) -> {
-            verifyDoesNotHavePermissionForStopTetheringRequest(request, result);
+            verifyStopTetheringRequestFails(WIFI_REQUEST_NO_CONFIG, result,
+                    TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
         });
 
         runAsTetherPrivileged((result) -> {
-            verifyHasPermissionForStopTetheringRequest(request, result);
+            verifyStopTetheringRequestSucceeds(WIFI_REQUEST_NO_CONFIG, result);
+        });
+
+        runAsTetherPrivileged((result) -> {
+            verifyStopTetheringRequestWithWrongPackageFails(WIFI_REQUEST_NO_CONFIG, result);
         });
 
         runAsWriteSettings((result) -> {
-            verifyDoesNotHavePermissionForStopTetheringRequest(request, result);
+            verifyStopTetheringRequestFails(WIFI_REQUEST_NO_CONFIG, result,
+                    TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
         });
 
         runAsWriteSettingsWhenWriteSettingsAllowed((result) -> {
-            verifyHasPermissionForStopTetheringRequest(request, result);
+            // Note: This can't happen in practice since WRITE_SETTINGS is only allowed on V- and
+            // stopTetheringRequest is only allowed on B+, but we test here for completeness.
+            verifyStopTetheringRequestSucceeds(WIFI_REQUEST_NO_CONFIG, result);
         });
 
         runAsTetheringDisallowed((result) -> {
-            verifyStopTetheringRequestWithTetheringDisallowed(request, result);
+            verifyStopTetheringRequestFails(WIFI_REQUEST_NO_CONFIG, result,
+                    TETHER_ERROR_UNSUPPORTED);
         });
 
         runAsNetworkSettings((result) -> {
-            verifyHasPermissionForStopTetheringRequest(request, result);
+            verifyStopTetheringRequestSucceeds(WIFI_REQUEST_NO_CONFIG, result);
         });
 
         // Not wifi -> fail
         runAsDeviceOwner((result) -> {
-            TetheringRequest notWifi = new TetheringRequest.Builder(TETHERING_USB).build();
-            verifyDoesNotHavePermissionForStopTetheringRequest(notWifi, result);
+            verifyStopTetheringRequestFails(USB_REQUEST, result,
+                    TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
         });
 
-        // No config -> fail
+        // No SoftApConfiguration -> fail
         runAsDeviceOwner((result) -> {
-            TetheringRequest noConfig = new TetheringRequest.Builder(TETHERING_WIFI).build();
-            verifyDoesNotHavePermissionForStopTetheringRequest(noConfig, result);
+            verifyStopTetheringRequestFails(WIFI_REQUEST_NO_CONFIG, result,
+                    TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
         });
 
-        // With config -> success
-        TetheringRequest withConfig = new TetheringRequest.Builder(TETHERING_WIFI)
-                .setSoftApConfiguration(new SoftApConfiguration.Builder().build())
-                .build();
+        // With SoftApConfiguration -> success
         runAsDeviceOwner((result) -> {
-            verifyHasPermissionForStopTetheringRequest(withConfig, result);
+            verifyStopTetheringRequestSucceeds(WIFI_REQUEST_WITH_CONFIG, result);
         });
 
         runAsDeviceOwnerWhenDeviceOwnerBypassNotEnabled((result) -> {
-            verifyDoesNotHavePermissionForStopTetheringRequest(withConfig, result);
+            verifyStopTetheringRequestFails(WIFI_REQUEST_WITH_CONFIG, result,
+                    TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+        });
+
+        // Not wifi -> fail
+        runAsCarrierPrivileged((result) -> {
+            verifyStopTetheringRequestFails(USB_REQUEST, result,
+                    TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+        });
+
+        // No SoftApConfiguration -> fail
+        runAsCarrierPrivileged((result) -> {
+            verifyStopTetheringRequestFails(WIFI_REQUEST_NO_CONFIG, result,
+                    TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+        });
+
+        // With SoftApConfiguration -> success
+        runAsCarrierPrivileged((result) -> {
+            verifyStopTetheringRequestSucceeds(WIFI_REQUEST_WITH_CONFIG, result);
+        });
+
+        runAsCarrierPrivilegedWhenCarrierPrivilegeBypassNotEnabled((result) -> {
+            verifyStopTetheringRequestFails(WIFI_REQUEST_WITH_CONFIG, result,
+                    TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
         });
     }
 
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/TetheringPermissionsUtilsTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/TetheringPermissionsUtilsTest.java
index 57c3eca..2b70e39 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/TetheringPermissionsUtilsTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/TetheringPermissionsUtilsTest.java
@@ -67,4 +67,15 @@
         when(mDevicePolicyManager.isDeviceOwnerApp(TEST_PACKAGE)).thenReturn(true);
         assertThat(mTetheringPermissionsUtils.isDeviceOwner(TEST_UID, TEST_PACKAGE)).isTrue();
     }
+
+    @Test
+    public void testHasCarrierPrivilege() {
+        when(mTelephonyManager.checkCarrierPrivilegesForPackageAnyPhone(TEST_PACKAGE))
+                .thenReturn(TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS);
+        assertThat(mTetheringPermissionsUtils.isCarrierPrivileged(TEST_PACKAGE)).isFalse();
+
+        when(mTelephonyManager.checkCarrierPrivilegesForPackageAnyPhone(TEST_PACKAGE))
+                .thenReturn(TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS);
+        assertThat(mTetheringPermissionsUtils.isCarrierPrivileged(TEST_PACKAGE)).isTrue();
+    }
 }
diff --git a/bpf/loader/NetBpfLoad.cpp b/bpf/loader/NetBpfLoad.cpp
index d70a2c8..63de1a6 100644
--- a/bpf/loader/NetBpfLoad.cpp
+++ b/bpf/loader/NetBpfLoad.cpp
@@ -556,9 +556,9 @@
         vector<string> csSymNames;
         ret = getSectionSymNames(elfFile, oldName, csSymNames, STT_FUNC);
         if (ret || !csSymNames.size()) return ret;
-        for (size_t i = 0; i < progDefNames.size(); ++i) {
-            if (!progDefNames[i].compare(csSymNames[0] + "_def")) {
-                cs_temp.prog_def = pd[i];
+        for (size_t j = 0; j < progDefNames.size(); ++j) {
+            if (!progDefNames[j].compare(csSymNames[0] + "_def")) {
+                cs_temp.prog_def = pd[j];
                 break;
             }
         }
@@ -769,7 +769,7 @@
     const size_t max_name = 256;
     char kvTypeName[max_name];
     int64_t keySize, valueSize;
-    uint32_t kvId;
+    int32_t kvId;
 
     if (snprintf(kvTypeName, max_name, "____btf_map_%s", mapName) == max_name) {
         ALOGE("____btf_map_%s is too long", mapName);
@@ -858,14 +858,16 @@
 
     struct btf *btf = NULL;
     auto scopeGuard = base::make_scope_guard([btf] { if (btf) btf__free(btf); });
-    if (isAtLeastKernelVersion(4, 18, 0)) {
+    if (isAtLeastKernelVersion(5, 10, 0)) {
+        // Untested on Linux Kernel 5.4, but likely compatible.
         // On Linux Kernels older than 4.18 BPF_BTF_LOAD command doesn't exist.
+        // On Linux Kernels older than 5.2 BTF_KIND_VAR and BTF_KIND_DATASEC don't exist.
         ret = readSectionByName(".BTF", elfFile, btfData);
         if (ret) {
             ALOGE("Failed to read .BTF section, ret:%d", ret);
             return ret;
         }
-        struct btf *btf = btf__new(btfData.data(), btfData.size());
+        btf = btf__new(btfData.data(), btfData.size());
         if (btf == NULL) {
             ALOGE("btf__new failed, errno: %d", errno);
             return -errno;
diff --git a/bpf/loader/netbpfload.rc b/bpf/loader/netbpfload.rc
index 10bfbb2..4cc6284 100644
--- a/bpf/loader/netbpfload.rc
+++ b/bpf/loader/netbpfload.rc
@@ -1,3 +1,5 @@
+# 2025 2 36 0 0 # 25q2 sdk/api level 36.0 - Android 16 Baklava QPR0
+
 # Note: This will actually execute /apex/com.android.tethering/bin/netbpfload
 # by virtue of 'service bpfloader' being overridden by the apex shipped .rc
 # Warning: most of the below settings are irrelevant unless the apex is missing.
diff --git a/bpf/netd/BpfHandler.cpp b/bpf/netd/BpfHandler.cpp
index d41aa81..680c05e 100644
--- a/bpf/netd/BpfHandler.cpp
+++ b/bpf/netd/BpfHandler.cpp
@@ -268,6 +268,16 @@
     RETURN_IF_NOT_OK(initMaps());
 
     if (isAtLeast25Q2) {
+        struct rlimit limit = {
+            .rlim_cur = 1u << 30,  // 1 GiB
+            .rlim_max = 1u << 30,  // 1 GiB
+        };
+        // 25Q2 netd.rc includes "rlimit memlock 1073741824 1073741824"
+        // so this should be a no-op, and thus just succeed.
+        // make sure it isn't lowered in platform netd.rc...
+        if (setrlimit(RLIMIT_MEMLOCK, &limit))
+            return statusFromErrno(errno, "Failed to set 1GiB RLIMIT_MEMLOCK");
+
         // Make sure netd can create & write maps.  sepolicy is V+, but enough to enforce on 25Q2+
         int key = 1;
         int value = 123;
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index c6b62ee..8355d31 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -360,6 +360,8 @@
         mUnderlyingNetworks = null;
         mEnterpriseId = 0;
         mReservationId = RES_ID_UNSET;
+        // TODO: Change to default disabled when introduce this filtering.
+        mMatchNonThreadLocalNetworks = true;
     }
 
     /**
@@ -395,6 +397,7 @@
         mUnderlyingNetworks = nc.mUnderlyingNetworks;
         mEnterpriseId = nc.mEnterpriseId;
         mReservationId = nc.mReservationId;
+        mMatchNonThreadLocalNetworks = nc.mMatchNonThreadLocalNetworks;
     }
 
     /**
@@ -2236,7 +2239,8 @@
                 && (onlyImmutable || satisfiedBySSID(nc))
                 && (onlyImmutable || satisfiedByRequestor(nc))
                 && (onlyImmutable || satisfiedBySubscriptionIds(nc)))
-                && satisfiedByReservationId(nc);
+                && satisfiedByReservationId(nc)
+                && satisfiedByMatchNonThreadLocalNetworks(nc);
     }
 
     /**
@@ -2351,7 +2355,8 @@
                 && equalsSubscriptionIds(that)
                 && equalsUnderlyingNetworks(that)
                 && equalsEnterpriseCapabilitiesId(that)
-                && equalsReservationId(that);
+                && equalsReservationId(that)
+                && equalsMatchNonThreadLocalNetworks(that);
     }
 
     @Override
@@ -2371,15 +2376,15 @@
                 + Objects.hashCode(mAllowedUids) * 41
                 + Objects.hashCode(mSSID) * 43
                 + Objects.hashCode(mTransportInfo) * 47
-                + Objects.hashCode(mPrivateDnsBroken) * 53
+                + Boolean.hashCode(mPrivateDnsBroken) * 53
                 + Objects.hashCode(mRequestorUid) * 59
                 + Objects.hashCode(mRequestorPackageName) * 61
                 + Arrays.hashCode(mAdministratorUids) * 67
                 + Objects.hashCode(mSubIds) * 71
                 + Objects.hashCode(mUnderlyingNetworks) * 73
                 + mEnterpriseId * 79
-                + mReservationId * 83;
-
+                + mReservationId * 83
+                + Boolean.hashCode(mMatchNonThreadLocalNetworks) * 89;
     }
 
     @Override
@@ -2418,6 +2423,7 @@
         dest.writeTypedList(mUnderlyingNetworks);
         dest.writeInt(mEnterpriseId & ALL_VALID_ENTERPRISE_IDS);
         dest.writeInt(mReservationId);
+        dest.writeBoolean(mMatchNonThreadLocalNetworks);
     }
 
     public static final @android.annotation.NonNull Creator<NetworkCapabilities> CREATOR =
@@ -2454,8 +2460,10 @@
                 netCap.setUnderlyingNetworks(in.createTypedArrayList(Network.CREATOR));
                 netCap.mEnterpriseId = in.readInt() & ALL_VALID_ENTERPRISE_IDS;
                 netCap.mReservationId = in.readInt();
+                netCap.mMatchNonThreadLocalNetworks = in.readBoolean();
                 return netCap;
             }
+
             @Override
             public NetworkCapabilities[] newArray(int size) {
                 return new NetworkCapabilities[size];
@@ -2561,6 +2569,10 @@
             sb.append(" ReservationId: ").append(isReservationOffer ? "*" : mReservationId);
         }
 
+        if (mMatchNonThreadLocalNetworks) {
+            sb.append(" MatchNonThreadLocalNetworks");
+        }
+
         sb.append(" UnderlyingNetworks: ");
         if (mUnderlyingNetworks != null) {
             sb.append("[");
@@ -2945,7 +2957,45 @@
         return mReservationId == nc.mReservationId;
     }
 
+    /**
+     * Flag to control whether a NetworkRequest can match non-thread local networks.
+     * @hide
+     */
+    // TODO: Change to default disabled when introduce this filtering.
+    private boolean mMatchNonThreadLocalNetworks = true;
 
+    /**
+     * Returns the match non-thread local networks flag.
+     *
+     * @hide
+     */
+    public boolean getMatchNonThreadLocalNetworks() {
+        return mMatchNonThreadLocalNetworks;
+    }
+
+    /**
+     * Set the match non-thread local networks flag.
+     * @hide
+     */
+    public void setMatchNonThreadLocalNetworks(boolean enabled) {
+        mMatchNonThreadLocalNetworks = enabled;
+    }
+
+    private boolean equalsMatchNonThreadLocalNetworks(@NonNull NetworkCapabilities nc) {
+        return mMatchNonThreadLocalNetworks == nc.mMatchNonThreadLocalNetworks;
+    }
+
+    // If the flag was set, the NetworkRequest can match all local networks.
+    // Otherwise, it can only see local networks created by Thread.
+    @SuppressWarnings("FlaggedApi")
+    private boolean satisfiedByMatchNonThreadLocalNetworks(@NonNull NetworkCapabilities nc) {
+        // If the network is not a local network, out of scope.
+        if (!nc.hasCapability(NET_CAPABILITY_LOCAL_NETWORK)) return true;
+        // If there is no restriction on matching non-thread local networks, return.
+        if (mMatchNonThreadLocalNetworks) return true;
+
+        return nc.hasTransport(TRANSPORT_THREAD);
+    }
 
     /**
      * Returns a bitmask of all the applicable redactions (based on the permissions held by the
diff --git a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
index 2261c69..3b2520e 100644
--- a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
+++ b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
@@ -139,13 +139,13 @@
 
     /**
      * Restrict local network access.
-     *
      * Apps targeting a release after V will require permissions to access the local network.
      *
+     * ToDo: Update the target SDK version once it's finalized.
      * @hide
      */
     @ChangeId
-    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT)
+    @EnabledAfter(targetSdkVersion = 36)
     public static final long RESTRICT_LOCAL_NETWORK = 365139289L;
 
     private ConnectivityCompatChanges() {
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 9b3c7ba..48467ed 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -755,7 +755,17 @@
     private void parseEthernetConfig(String configString) {
         final EthernetTrackerConfig config = createEthernetTrackerConfig(configString);
         NetworkCapabilities nc;
-        if (TextUtils.isEmpty(config.mCapabilities)) {
+        // Starting with Android B (API level 36), we provide default NetworkCapabilities
+        // for Ethernet interfaces when no explicit capabilities are specified in the
+        // configuration string. This change is made to ensure consistent and expected
+        // network behavior for Ethernet devices.
+        //
+        // It's possible that OEMs or device manufacturers may have relied on the previous
+        // behavior (where interfaces without specified capabilities would have minimal
+        // capabilities) to prevent certain Ethernet interfaces from becoming
+        // the default network. To avoid breaking existing device configurations, this
+        // change is gated by the SDK level.
+        if (SdkLevel.isAtLeastB() && TextUtils.isEmpty(config.mCapabilities)) {
             boolean isTestIface = config.mIface.matches(TEST_IFACE_REGEXP);
             nc = createDefaultNetworkCapabilities(isTestIface, config.mTransport);
         } else {
diff --git a/service-t/src/com/android/server/net/NetworkStatsFactory.java b/service-t/src/com/android/server/net/NetworkStatsFactory.java
index 5ff708d..c5a69c0 100644
--- a/service-t/src/com/android/server/net/NetworkStatsFactory.java
+++ b/service-t/src/com/android/server/net/NetworkStatsFactory.java
@@ -19,6 +19,7 @@
 import static android.net.NetworkStats.INTERFACES_ALL;
 import static android.net.NetworkStats.TAG_ALL;
 import static android.net.NetworkStats.UID_ALL;
+import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
 
 import android.annotation.NonNull;
 import android.content.Context;
@@ -26,15 +27,26 @@
 import android.net.UnderlyingNetworkInfo;
 import android.os.ServiceSpecificException;
 import android.os.SystemClock;
+import android.util.ArraySet;
+import android.util.IndentingPrintWriter;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.DeviceConfigUtils;
 import com.android.server.BpfNetMaps;
 import com.android.server.connectivity.InterfaceTracker;
 
 import java.io.IOException;
 import java.net.ProtocolException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
 /**
@@ -65,6 +77,18 @@
     /** Set containing info about active VPNs and their underlying networks. */
     private volatile UnderlyingNetworkInfo[] mUnderlyingNetworkInfos = new UnderlyingNetworkInfo[0];
 
+    static final String CONFIG_PER_UID_TAG_THROTTLING = "per_uid_tag_throttling";
+    static final String CONFIG_PER_UID_TAG_THROTTLING_THRESHOLD =
+            "per_uid_tag_throttling_threshold";
+    private static final int DEFAULT_TAGS_PER_UID_THRESHOLD = 1000;
+    private static final int DUMP_TAGS_PER_UID_COUNT = 20;
+    private final boolean mSupportPerUidTagThrottling;
+    private final int mPerUidTagThrottlingThreshold;
+
+    // Map for set of distinct tags per uid. Used for tag count limiting.
+    @GuardedBy("mPersistentDataLock")
+    private final SparseArray<SparseBooleanArray> mUidTagSets = new SparseArray<>();
+
     // A persistent snapshot of cumulative stats since device start
     @GuardedBy("mPersistentDataLock")
     private NetworkStats mPersistSnapshot;
@@ -110,6 +134,26 @@
         public BpfNetMaps createBpfNetMaps(@NonNull Context ctx) {
             return new BpfNetMaps(ctx, new InterfaceTracker(ctx));
         }
+
+        /**
+         * Check whether one specific feature is not disabled.
+         * @param name Flag name of the experiment in the tethering namespace.
+         * @see DeviceConfigUtils#isTetheringFeatureNotChickenedOut(Context, String)
+         */
+        public boolean isFeatureNotChickenedOut(@NonNull Context context, @NonNull String name) {
+            return DeviceConfigUtils.isTetheringFeatureNotChickenedOut(context, name);
+        }
+
+        /**
+         * Wrapper method for DeviceConfigUtils#getDeviceConfigPropertyInt for test injections.
+         *
+         * See {@link DeviceConfigUtils#getDeviceConfigPropertyInt(String, String, int)}
+         * for more detailed information.
+         */
+        public int getDeviceConfigPropertyInt(@NonNull String name, int defaultValue) {
+            return DeviceConfigUtils.getDeviceConfigPropertyInt(
+                    NAMESPACE_TETHERING, name, defaultValue);
+        }
     }
 
     /**
@@ -162,6 +206,10 @@
         }
         mContext = ctx;
         mDeps = deps;
+        mSupportPerUidTagThrottling = mDeps.isFeatureNotChickenedOut(
+            ctx, CONFIG_PER_UID_TAG_THROTTLING);
+        mPerUidTagThrottlingThreshold = mDeps.getDeviceConfigPropertyInt(
+                CONFIG_PER_UID_TAG_THROTTLING_THRESHOLD, DEFAULT_TAGS_PER_UID_THRESHOLD);
     }
 
     /**
@@ -210,10 +258,13 @@
             requestSwapActiveStatsMapLocked();
             // Stats are always read from the inactive map, so they must be read after the
             // swap
-            final NetworkStats stats = mDeps.getNetworkStatsDetail();
+            final NetworkStats diff = mDeps.getNetworkStatsDetail();
+            // Filter based on UID tag set before merging.
+            final NetworkStats filteredDiff = mSupportPerUidTagThrottling
+                    ? filterStatsByUidTagSets(diff) : diff;
             // BPF stats are incremental; fold into mPersistSnapshot.
-            mPersistSnapshot.setElapsedRealtime(stats.getElapsedRealtime());
-            mPersistSnapshot.combineAllValues(stats);
+            mPersistSnapshot.setElapsedRealtime(diff.getElapsedRealtime());
+            mPersistSnapshot.combineAllValues(filteredDiff);
 
             NetworkStats adjustedStats = adjustForTunAnd464Xlat(mPersistSnapshot, prev, vpnArray);
 
@@ -224,6 +275,41 @@
     }
 
     @GuardedBy("mPersistentDataLock")
+    private NetworkStats filterStatsByUidTagSets(NetworkStats stats) {
+        final NetworkStats filteredStats =
+                new NetworkStats(stats.getElapsedRealtime(), stats.size());
+
+        final NetworkStats.Entry entry = new NetworkStats.Entry();
+        final Set<Integer> tooManyTagsUidSet = new ArraySet<>();
+        for (int i = 0; i < stats.size(); i++) {
+            stats.getValues(i, entry);
+            final int uid = entry.uid;
+            final int tag = entry.tag;
+
+            if (tag == NetworkStats.TAG_NONE) {
+                filteredStats.combineValues(entry);
+                continue;
+            }
+
+            SparseBooleanArray tagSet = mUidTagSets.get(uid);
+            if (tagSet == null) {
+                tagSet = new SparseBooleanArray();
+            }
+            if (tagSet.size() < mPerUidTagThrottlingThreshold || tagSet.get(tag)) {
+                filteredStats.combineValues(entry);
+                tagSet.put(tag, true);
+                mUidTagSets.put(uid, tagSet);
+            } else {
+                tooManyTagsUidSet.add(uid);
+            }
+        }
+        if (tooManyTagsUidSet.size() > 0) {
+            Log.wtf(TAG, "Too many tags detected for uids: " + tooManyTagsUidSet);
+        }
+        return filteredStats;
+    }
+
+    @GuardedBy("mPersistentDataLock")
     private NetworkStats adjustForTunAnd464Xlat(NetworkStats uidDetailStats,
             NetworkStats previousStats, UnderlyingNetworkInfo[] vpnArray) {
         // Calculate delta from last snapshot
@@ -307,4 +393,34 @@
         pe.initCause(cause);
         return pe;
     }
+
+    /**
+     * Dump the contents of NetworkStatsFactory.
+     */
+    public void dump(IndentingPrintWriter pw) {
+        dumpUidTagSets(pw);
+    }
+
+    private void dumpUidTagSets(IndentingPrintWriter pw) {
+        pw.println("Top distinct tag counts in UidTagSets:");
+        pw.increaseIndent();
+        final List<Pair<Integer, Integer>> countForUidList = new ArrayList<>();
+        synchronized (mPersistentDataLock) {
+            for (int i = 0; i < mUidTagSets.size(); i++) {
+                final Pair<Integer, Integer> countForUid =
+                        new Pair<>(mUidTagSets.keyAt(i), mUidTagSets.valueAt(i).size());
+                countForUidList.add(countForUid);
+            }
+        }
+        Collections.sort(countForUidList,
+                (entry1, entry2) -> Integer.compare(entry2.second, entry1.second));
+        final int dumpSize = Math.min(countForUidList.size(), DUMP_TAGS_PER_UID_COUNT);
+        for (int j = 0; j < dumpSize; j++) {
+            final Pair<Integer, Integer> entry = countForUidList.get(j);
+            pw.print(entry.first);
+            pw.print("=");
+            pw.println(entry.second);
+        }
+        pw.decreaseIndent();
+    }
 }
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 5c5f4ca..75d30a9 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -3228,6 +3228,12 @@
             pw.increaseIndent();
             mSkDestroyListener.dump(pw);
             pw.decreaseIndent();
+
+            pw.println();
+            pw.println("NetworkStatsFactory logs:");
+            pw.increaseIndent();
+            mStatsFactory.dump(pw);
+            pw.decreaseIndent();
         }
     }
 
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index f6dbf6c..28b46c1 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -49,6 +49,7 @@
             <!-- Configuration values for ThreadNetworkService -->
             <item type="bool" name="config_thread_default_enabled" />
             <item type="bool" name="config_thread_border_router_default_enabled" />
+            <item type="bool" name="config_thread_country_code_enabled" />
             <item type="bool" name="config_thread_location_use_for_country_code_enabled" />
             <item type="string" name="config_thread_vendor_name" />
             <item type="string" name="config_thread_vendor_oui" />
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index bfb51da..b9b590b 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -113,6 +113,7 @@
 import static android.net.NetworkCapabilities.RES_ID_UNSET;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+import static android.net.NetworkCapabilities.TRANSPORT_THREAD;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.NetworkRequest.Type.LISTEN_FOR_BEST;
@@ -429,6 +430,7 @@
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
+import java.util.function.Predicate;
 
 /**
  * @hide
@@ -4893,7 +4895,11 @@
                     // the destroyed flag is only just above the "current satisfier wins"
                     // tie-breaker. But technically anything that affects scoring should rematch.
                     rematchAllNetworksAndRequests();
-                    mHandler.postDelayed(() -> nai.disconnect(), timeoutMs);
+                    if (mQueueNetworkAgentEventsInSystemServer) {
+                        mHandler.postDelayed(() -> disconnectAndDestroyNetwork(nai), timeoutMs);
+                    } else {
+                        mHandler.postDelayed(() -> nai.disconnect(), timeoutMs);
+                    }
                     break;
                 }
             }
@@ -5323,12 +5329,12 @@
     private void handlePrivateDnsSettingsChanged() {
         final PrivateDnsConfig cfg = mDnsManager.getPrivateDnsConfig();
 
-        for (NetworkAgentInfo nai : mNetworkAgentInfos) {
+        forEachNetworkAgentInfo(nai -> {
             handlePerNetworkPrivateDnsConfig(nai, cfg);
             if (networkRequiresPrivateDnsValidation(nai)) {
                 handleUpdateLinkProperties(nai, new LinkProperties(nai.linkProperties));
             }
-        }
+        });
     }
 
     private void handlePerNetworkPrivateDnsConfig(NetworkAgentInfo nai, PrivateDnsConfig cfg) {
@@ -5443,15 +5449,18 @@
     }
 
     @VisibleForTesting
-    protected static boolean shouldCreateNetworksImmediately(@NonNull NetworkCapabilities caps) {
+    protected boolean shouldCreateNetworksImmediately(@NonNull NetworkCapabilities caps) {
         // The feature of creating the networks immediately was slated for U, but race conditions
         // detected late required this was flagged off.
-        // TODO : enable this in a Mainline update or in V, and re-enable the test for this
-        // in NetworkAgentTest.
-        return caps.hasCapability(NET_CAPABILITY_LOCAL_NETWORK);
+        // TODO : remove when it's determined that the code is stable
+        return mQueueNetworkAgentEventsInSystemServer
+                // Local network agents for Thread used to not create networks immediately,
+                // but other local agents (tethering, P2P) require this to function.
+                || (caps.hasCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                && !caps.hasTransport(TRANSPORT_THREAD));
     }
 
-    private static boolean shouldCreateNativeNetwork(@NonNull NetworkAgentInfo nai,
+    private boolean shouldCreateNativeNetwork(@NonNull NetworkAgentInfo nai,
             @NonNull NetworkInfo.State state) {
         if (nai.isCreated()) return false;
         if (state == NetworkInfo.State.CONNECTED) return true;
@@ -5508,6 +5517,11 @@
         if (DBG) {
             log(nai.toShortString() + " disconnected, was satisfying " + nai.numNetworkRequests());
         }
+
+        if (mQueueNetworkAgentEventsInSystemServer) {
+            nai.disconnect();
+        }
+
         // Clear all notifications of this network.
         mNotifier.clearNotification(nai.network.getNetId());
         // A network agent has disconnected.
@@ -5651,16 +5665,16 @@
     private void maybeDisableForwardRulesForDisconnectingNai(
             @NonNull final NetworkAgentInfo disconnecting, final boolean sendCallbacks) {
         // Step 1 : maybe this network was the upstream for one or more local networks.
-        for (final NetworkAgentInfo local : mNetworkAgentInfos) {
-            if (!local.isLocalNetwork()) continue;
+        forEachNetworkAgentInfo(local -> {
+            if (!local.isLocalNetwork()) return; // return@forEach
             final NetworkRequest selector = local.localNetworkConfig.getUpstreamSelector();
-            if (null == selector) continue;
+            if (null == selector) return; // return@forEach
             final NetworkRequestInfo nri = mNetworkRequests.get(selector);
             // null == nri can happen while disconnecting a network, because destroyNetwork() is
             // called after removing all associated NRIs from mNetworkRequests.
-            if (null == nri) continue;
+            if (null == nri) return; // return@forEach
             final NetworkAgentInfo satisfier = nri.getSatisfier();
-            if (disconnecting != satisfier) continue;
+            if (disconnecting != satisfier) return; // return@forEach
             removeLocalNetworkUpstream(local, disconnecting);
             // Set the satisfier to null immediately so that the LOCAL_NETWORK_CHANGED callback
             // correctly contains null as an upstream.
@@ -5668,7 +5682,7 @@
                 nri.setSatisfier(null, null);
                 notifyNetworkCallbacks(local, CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
             }
-        }
+        });
 
         // Step 2 : maybe this is a local network that had an upstream.
         if (!disconnecting.isLocalNetwork()) return;
@@ -5841,12 +5855,12 @@
                 mNetworkRequests.put(req, nri);
                 // TODO: Consider update signal strength for other types.
                 if (req.isListen()) {
-                    for (final NetworkAgentInfo network : mNetworkAgentInfos) {
+                    forEachNetworkAgentInfo(network -> {
                         if (req.networkCapabilities.hasSignalStrength()
                                 && network.satisfiesImmutableCapabilitiesOf(req)) {
                             updateSignalStrengthThresholds(network, "REGISTER", req);
                         }
-                    }
+                    });
                 } else if (req.isRequest() && mNetworkRequestStateStatsMetrics != null) {
                     mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(req);
                 }
@@ -6141,13 +6155,13 @@
     private void removeListenRequestFromNetworks(@NonNull final NetworkRequest req) {
         // listens don't have a singular affected Network. Check all networks to see
         // if this listen request applies and remove it.
-        for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
+        forEachNetworkAgentInfo(nai -> {
             nai.removeRequest(req.requestId);
             if (req.networkCapabilities.hasSignalStrength()
                     && nai.satisfiesImmutableCapabilitiesOf(req)) {
                 updateSignalStrengthThresholds(nai, "RELEASE", req);
             }
-        }
+        });
     }
 
     /**
@@ -6210,6 +6224,43 @@
         }
     }
 
+    /**
+     * Perform the specified operation on all networks.
+     *
+     * This method will run |op| exactly once for each network currently registered at the
+     * time it is called, even if |op| adds or removes networks.
+     *
+     * @param op the operation to perform. The operation is allowed to disconnect any number of
+     *           networks.
+     */
+    private void forEachNetworkAgentInfo(final Consumer<NetworkAgentInfo> op) {
+        // Create a copy instead of iterating over the set so |op| is allowed to disconnect any
+        // number of networks (which removes it from mNetworkAgentInfos). The copy is cheap
+        // because there are at most a handful of NetworkAgents connected at any given time.
+        final NetworkAgentInfo[] nais = new NetworkAgentInfo[mNetworkAgentInfos.size()];
+        mNetworkAgentInfos.toArray(nais);
+        for (NetworkAgentInfo nai : nais) {
+            op.accept(nai);
+        }
+    }
+
+    /**
+     * Check whether the specified condition is true for any network.
+     *
+     * This method will stop evaluating as soon as the condition returns true for any network.
+     * The order of iteration is not contractual.
+     *
+     * @param condition the condition to verify. This method must not modify the set of networks in
+     *                  any way.
+     * @return whether {@code condition} returned true for any network
+     */
+    private boolean anyNetworkAgentInfo(final Predicate<NetworkAgentInfo> condition) {
+        for (int i = mNetworkAgentInfos.size() - 1; i >= 0; i--) {
+            if (condition.test(mNetworkAgentInfos.valueAt(i))) return true;
+        }
+        return false;
+    }
+
     private RequestInfoPerUidCounter getRequestCounter(NetworkRequestInfo nri) {
         return hasAnyPermissionOf(mContext,
                 nri.mPid, nri.mUid, NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
@@ -6551,14 +6602,14 @@
         ensureRunningOnConnectivityServiceThread();
         // Agent info scores and offer scores depend on whether cells yields to bad wifi.
         final boolean avoidBadWifi = avoidBadWifi();
-        for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
+        forEachNetworkAgentInfo(nai -> {
             nai.updateScoreForNetworkAgentUpdate();
             if (avoidBadWifi) {
                 // If the device is now avoiding bad wifi, remove notifications that might have
                 // been put up when the device didn't.
                 mNotifier.clearNotification(nai.network.getNetId(), NotificationType.LOST_INTERNET);
             }
-        }
+        });
         // UpdateOfferScore will update mNetworkOffers inline, so make a copy first.
         final ArrayList<NetworkOfferInfo> offersToUpdate = new ArrayList<>(mNetworkOffers);
         for (final NetworkOfferInfo noi : offersToUpdate) {
@@ -6896,19 +6947,15 @@
 
                     final Network underpinnedNetwork = ki.getUnderpinnedNetwork();
                     final Network network = ki.getNetwork();
-                    boolean networkFound = false;
-                    boolean underpinnedNetworkFound = false;
-                    for (NetworkAgentInfo n : mNetworkAgentInfos) {
-                        if (n.network.equals(network)) networkFound = true;
-                        if (n.everConnected() && n.network.equals(underpinnedNetwork)) {
-                            underpinnedNetworkFound = true;
-                        }
-                    }
+                    final boolean networkFound =
+                            anyNetworkAgentInfo(n -> n.network.equals(network));
 
                     // If the network no longer exists, then the keepalive should have been
                     // cleaned up already. There is no point trying to resume keepalives.
                     if (!networkFound) return;
 
+                    final boolean underpinnedNetworkFound = anyNetworkAgentInfo(
+                            n -> n.everConnected() && n.network.equals(underpinnedNetwork));
                     if (underpinnedNetworkFound) {
                         mKeepaliveTracker.handleMonitorAutomaticKeepalive(ki,
                                 underpinnedNetwork.netId);
@@ -6978,7 +7025,11 @@
                     final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork((Network) msg.obj);
                     if (nai == null) break;
                     nai.onPreventAutomaticReconnect();
-                    nai.disconnect();
+                    if (mQueueNetworkAgentEventsInSystemServer) {
+                        disconnectAndDestroyNetwork(nai);
+                    } else {
+                        nai.disconnect();
+                    }
                     break;
                 case EVENT_SET_VPN_NETWORK_PREFERENCE:
                     handleSetVpnNetworkPreference((VpnNetworkPreferenceInfo) msg.obj);
@@ -7368,12 +7419,12 @@
             return new UnderlyingNetworkInfo[0];
         }
         List<UnderlyingNetworkInfo> infoList = new ArrayList<>();
-        for (NetworkAgentInfo nai : mNetworkAgentInfos) {
+        forEachNetworkAgentInfo(nai -> {
             UnderlyingNetworkInfo info = createVpnInfo(nai);
             if (info != null) {
                 infoList.add(info);
             }
-        }
+        });
         return infoList.toArray(new UnderlyingNetworkInfo[infoList.size()]);
     }
 
@@ -7451,11 +7502,11 @@
      */
     private void propagateUnderlyingNetworkCapabilities(Network updatedNetwork) {
         ensureRunningOnConnectivityServiceThread();
-        for (NetworkAgentInfo nai : mNetworkAgentInfos) {
+        forEachNetworkAgentInfo(nai -> {
             if (updatedNetwork == null || hasUnderlyingNetwork(nai, updatedNetwork)) {
                 updateCapabilitiesForNetwork(nai);
             }
-        }
+        });
     }
 
     private boolean isUidBlockedByVpn(int uid, List<UidRange> blockedUidRanges) {
@@ -7503,11 +7554,11 @@
             mPermissionMonitor.updateVpnLockdownUidRanges(requireVpn, ranges);
         }
 
-        for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
+        forEachNetworkAgentInfo(nai -> {
             final boolean curMetered = nai.networkCapabilities.isMetered();
             maybeNotifyNetworkBlocked(nai, curMetered, curMetered,
                     mVpnBlockedUidRanges, newVpnBlockedUidRanges);
-        }
+        });
 
         mVpnBlockedUidRanges = newVpnBlockedUidRanges;
     }
@@ -9071,6 +9122,9 @@
 
     // Tracks all NetworkAgents that are currently registered.
     // NOTE: Only should be accessed on ConnectivityServiceThread, except dump().
+    // Code iterating over this set is recommended to use forAllNetworkAgentInfos(), which allows
+    // code within the loop to disconnect networks during iteration without causing null pointer or
+    // OOB exceptions.
     private final ArraySet<NetworkAgentInfo> mNetworkAgentInfos = new ArraySet<>();
 
     // UID ranges for users that are currently blocked by VPNs.
@@ -10439,7 +10493,7 @@
 
         // A NetworkAgent's allowedUids may need to be updated if the app has lost
         // carrier config
-        for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
+        forEachNetworkAgentInfo(nai -> {
             if (nai.networkCapabilities.getAllowedUidsNoCopy().contains(uid)
                     && getSubscriptionIdFromNetworkCaps(nai.networkCapabilities) == subId) {
                 final NetworkCapabilities nc = new NetworkCapabilities(nai.networkCapabilities);
@@ -10451,7 +10505,7 @@
                         mCarrierPrivilegeAuthenticator);
                 updateCapabilities(nai.getScore(), nai, nc);
             }
-        }
+        });
     }
 
     /**
@@ -11208,7 +11262,11 @@
                 break;
             }
         }
-        nai.disconnect();
+        if (mQueueNetworkAgentEventsInSystemServer) {
+            disconnectAndDestroyNetwork(nai);
+        } else {
+            nai.disconnect();
+        }
     }
 
     private void handleLingerComplete(NetworkAgentInfo oldNetwork) {
@@ -11368,7 +11426,7 @@
             throw new IllegalStateException("No user is available");
         }
 
-        for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
+        forEachNetworkAgentInfo(nai -> {
             ArraySet<UidRange> allowedUidRanges = new ArraySet<>();
             for (final UserHandle user : users) {
                 final ArraySet<UidRange> restrictedUidRanges =
@@ -11380,7 +11438,7 @@
             final UidRangeParcel[] rangesParcel = toUidRangeStableParcels(allowedUidRanges);
             configs.add(new NativeUidRangeConfig(
                     nai.network.netId, rangesParcel, 0 /* subPriority */));
-        }
+        });
 
         // The netd API replaces the previous configs with the current configs.
         // Thus, for network disconnection or preference removal, no need to
@@ -11602,9 +11660,7 @@
 
         // Gather the list of all relevant agents.
         final ArrayList<NetworkAgentInfo> nais = new ArrayList<>();
-        for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
-            nais.add(nai);
-        }
+        forEachNetworkAgentInfo(nai -> nais.add(nai));
 
         for (final NetworkRequestInfo nri : networkRequests) {
             // Non-multilayer listen requests can be ignored.
@@ -11710,14 +11766,14 @@
         // Don't send CALLBACK_LOCAL_NETWORK_INFO_CHANGED yet though : they should be sent after
         // onAvailable so clients know what network the change is about. Store such changes in
         // an array that's only allocated if necessary (because it's almost never necessary).
-        ArrayList<NetworkAgentInfo> localInfoChangedAgents = null;
-        for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
-            if (!nai.isLocalNetwork()) continue;
+        final ArrayList<NetworkAgentInfo> localInfoChangedAgents = new ArrayList<>();
+        forEachNetworkAgentInfo(nai -> {
+            if (!nai.isLocalNetwork()) return; // return@forEach
             final NetworkRequest nr = nai.localNetworkConfig.getUpstreamSelector();
-            if (null == nr) continue; // No upstream for this local network
+            if (null == nr) return; // return@forEach, no upstream for this local network
             final NetworkRequestInfo nri = mNetworkRequests.get(nr);
             final NetworkReassignment.RequestReassignment change = changes.getReassignment(nri);
-            if (null == change) continue; // No change in upstreams for this network
+            if (null == change) return; // return@forEach, no change in upstreams for this network
             final String fromIface = nai.linkProperties.getInterfaceName();
             if (!hasSameInterfaceName(change.mOldNetwork, change.mNewNetwork)
                     || change.mOldNetwork.isDestroyed()) {
@@ -11745,9 +11801,8 @@
                     loge("Can't update forwarding rules", e);
                 }
             }
-            if (null == localInfoChangedAgents) localInfoChangedAgents = new ArrayList<>();
             localInfoChangedAgents.add(nai);
-        }
+        });
 
         // Notify requested networks are available after the default net is switched, but
         // before LegacyTypeTracker sends legacy broadcasts
@@ -11798,17 +11853,14 @@
         }
 
         // Send LOCAL_NETWORK_INFO_CHANGED callbacks now that onAvailable and onLost have been sent.
-        if (null != localInfoChangedAgents) {
-            for (final NetworkAgentInfo nai : localInfoChangedAgents) {
-                notifyNetworkCallbacks(nai,
-                        CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
-            }
+        for (final NetworkAgentInfo nai : localInfoChangedAgents) {
+          notifyNetworkCallbacks(nai, CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
         }
 
         updateLegacyTypeTrackerAndVpnLockdownForRematch(changes, nais);
 
         // Tear down all unneeded networks.
-        for (NetworkAgentInfo nai : mNetworkAgentInfos) {
+        forEachNetworkAgentInfo(nai -> {
             if (unneeded(nai, UnneededFor.TEARDOWN)) {
                 if (nai.getInactivityExpiry() > 0) {
                     // This network has active linger timers and no requests, but is not
@@ -11826,7 +11878,7 @@
                     teardownUnneededNetwork(nai);
                 }
             }
-        }
+        });
     }
 
     /**
@@ -12215,7 +12267,9 @@
             // This has to happen after matching the requests, because callbacks are just requests.
             notifyNetworkCallbacks(networkAgent, CALLBACK_PRECHECK);
         } else if (state == NetworkInfo.State.DISCONNECTED) {
-            networkAgent.disconnect();
+            if (!mQueueNetworkAgentEventsInSystemServer) {
+                networkAgent.disconnect();
+            }
             if (networkAgent.isVPN()) {
                 updateVpnUids(networkAgent, networkAgent.networkCapabilities, null);
             }
@@ -12339,7 +12393,7 @@
      * @param blockedReasons The reasons for why an uid is blocked.
      */
     private void maybeNotifyNetworkBlockedForNewState(int uid, @BlockedReason int blockedReasons) {
-        for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
+        forEachNetworkAgentInfo(nai -> {
             final boolean metered = nai.networkCapabilities.isMetered();
             final boolean vpnBlocked = isUidBlockedByVpn(uid, mVpnBlockedUidRanges);
 
@@ -12347,9 +12401,7 @@
                     uid, mUidBlockedReasons.get(uid, BLOCKED_REASON_NONE), metered, vpnBlocked);
             final int newBlockedState =
                     getBlockedState(uid, blockedReasons, metered, vpnBlocked);
-            if (oldBlockedState == newBlockedState) {
-                continue;
-            }
+            if (oldBlockedState == newBlockedState) return; // return@forEach
             for (int i = 0; i < nai.numNetworkRequests(); i++) {
                 NetworkRequest nr = nai.requestAt(i);
                 NetworkRequestInfo nri = mNetworkRequests.get(nr);
@@ -12358,7 +12410,7 @@
                             newBlockedState);
                 }
             }
-        }
+        });
     }
 
     @VisibleForTesting
@@ -12447,11 +12499,11 @@
                 activeNetIds.add(nri.getSatisfier().network().netId);
             }
         }
-        for (NetworkAgentInfo nai : mNetworkAgentInfos) {
+        forEachNetworkAgentInfo(nai -> {
             if (activeNetIds.contains(nai.network().netId) || nai.isVPN()) {
                 defaultNetworks.add(nai.network);
             }
-        }
+        });
         return defaultNetworks;
     }
 
@@ -13342,15 +13394,10 @@
     }
 
     private boolean ownsVpnRunningOverNetwork(int uid, Network network) {
-        for (NetworkAgentInfo virtual : mNetworkAgentInfos) {
-            if (virtual.propagateUnderlyingCapabilities()
-                    && virtual.networkCapabilities.getOwnerUid() == uid
-                    && CollectionUtils.contains(virtual.declaredUnderlyingNetworks, network)) {
-                return true;
-            }
-        }
-
-        return false;
+        return anyNetworkAgentInfo(virtual ->
+                virtual.propagateUnderlyingCapabilities()
+                        && virtual.networkCapabilities.getOwnerUid() == uid
+                        && CollectionUtils.contains(virtual.declaredUnderlyingNetworks, network));
     }
 
     @CheckResult
@@ -13521,18 +13568,16 @@
         @Override
         public void onInterfaceLinkStateChanged(@NonNull String iface, boolean up) {
             mHandler.post(() -> {
-                for (NetworkAgentInfo nai : mNetworkAgentInfos) {
-                    nai.clatd.handleInterfaceLinkStateChanged(iface, up);
-                }
+                forEachNetworkAgentInfo(nai ->
+                        nai.clatd.handleInterfaceLinkStateChanged(iface, up));
             });
         }
 
         @Override
         public void onInterfaceRemoved(@NonNull String iface) {
             mHandler.post(() -> {
-                for (NetworkAgentInfo nai : mNetworkAgentInfos) {
-                    nai.clatd.handleInterfaceRemoved(iface);
-                }
+                forEachNetworkAgentInfo(nai ->
+                        nai.clatd.handleInterfaceRemoved(iface));
             });
         }
     }
@@ -14313,7 +14358,7 @@
         final long oldIngressRateLimit = mIngressRateLimit;
         mIngressRateLimit = ConnectivitySettingsManager.getIngressRateLimitInBytesPerSecond(
                 mContext);
-        for (final NetworkAgentInfo networkAgent : mNetworkAgentInfos) {
+        forEachNetworkAgentInfo(networkAgent -> {
             if (canNetworkBeRateLimited(networkAgent)) {
                 // If rate limit has previously been enabled, remove the old limit first.
                 if (oldIngressRateLimit >= 0) {
@@ -14324,7 +14369,7 @@
                             mIngressRateLimit);
                 }
             }
-        }
+        });
     }
 
     private boolean canNetworkBeRateLimited(@NonNull final NetworkAgentInfo networkAgent) {
diff --git a/service/src/com/android/server/L2capNetworkProvider.java b/service/src/com/android/server/L2capNetworkProvider.java
index 0352ad5..149979f 100644
--- a/service/src/com/android/server/L2capNetworkProvider.java
+++ b/service/src/com/android/server/L2capNetworkProvider.java
@@ -597,10 +597,12 @@
             final ClientRequestInfo cri = mClientNetworkRequests.get(specifier);
             if (cri == null) return;
 
+            // Release ClientNetworkRequest before sending onUnavailable() to ensure the app
+            // first receives an onLost() callback if a network had been created.
+            releaseClientNetworkRequest(cri);
             for (NetworkRequest request : cri.requests) {
                 mProvider.declareNetworkRequestUnfulfillable(request);
             }
-            releaseClientNetworkRequest(cri);
         }
     }
 
diff --git a/staticlibs/tests/unit/host/python/packet_utils_test.py b/staticlibs/tests/unit/host/python/packet_utils_test.py
deleted file mode 100644
index 8ad9576..0000000
--- a/staticlibs/tests/unit/host/python/packet_utils_test.py
+++ /dev/null
@@ -1,72 +0,0 @@
-#  Copyright (C) 2024 The Android Open Source Project
-#
-#  Licensed under the Apache License, Version 2.0 (the "License");
-#  you may not use this file except in compliance with the License.
-#  You may obtain a copy of the License at
-#
-#       http://www.apache.org/licenses/LICENSE-2.0
-#
-#  Unless required by applicable law or agreed to in writing, software
-#  distributed under the License is distributed on an "AS IS" BASIS,
-#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-#  See the License for the specific language governing permissions and
-#  limitations under the License.
-
-from mobly import asserts
-from mobly import base_test
-from net_tests_utils.host.python import packet_utils
-
-class TestPacketUtils(base_test.BaseTestClass):
-    def test_unicast_arp_request(self):
-        # Using scapy to generate unicast arp request packet:
-        #   eth = Ether(src="00:01:02:03:04:05", dst="01:02:03:04:05:06")
-        #   arp = ARP(op=1, pdst="192.168.1.1", hwsrc="00:01:02:03:04:05", psrc="192.168.1.2")
-        #   pkt = eth/arp
-        expect_arp_request = """
-            01020304050600010203040508060001080006040001000102030405c0a80102000000000000c0a80101
-        """.upper().replace(" ", "").replace("\n", "")
-        arp_request = packet_utils.construct_arp_packet(
-            src_mac="00:01:02:03:04:05",
-            dst_mac="01:02:03:04:05:06",
-            src_ip="192.168.1.2",
-            dst_ip="192.168.1.1",
-            op=packet_utils.ARP_REQUEST_OP
-        )
-        asserts.assert_equal(expect_arp_request, arp_request)
-
-    def test_broadcast_arp_request(self):
-        # Using scapy to generate unicast arp request packet:
-        #   eth = Ether(src="00:01:02:03:04:05", dst="FF:FF:FF:FF:FF:FF")
-        #   arp = ARP(op=1, pdst="192.168.1.1", hwsrc="00:01:02:03:04:05", psrc="192.168.1.2")
-        #   pkt = eth/arp
-        expect_arp_request = """
-            ffffffffffff00010203040508060001080006040001000102030405c0a80102000000000000c0a80101
-        """.upper().replace(" ", "").replace("\n", "")
-        arp_request = packet_utils.construct_arp_packet(
-            src_mac="00:01:02:03:04:05",
-            dst_mac=packet_utils.ETHER_BROADCAST_MAC_ADDRESS,
-            src_ip="192.168.1.2",
-            dst_ip="192.168.1.1",
-            op=packet_utils.ARP_REQUEST_OP
-        )
-        asserts.assert_equal(expect_arp_request, arp_request)
-
-    def test_arp_reply(self):
-        # Using scapy to generate unicast arp request packet:
-        #   eth = Ether(src="01:02:03:04:05:06", dst="00:01:02:03:04:05")
-        #   arp = ARP(op=2, pdst="192.168.1.2", \
-        #             hwsrc="01:02:03:04:05:06", \
-        #             psrc="192.168.1.1", \
-        #             hwdst="00:01:02:03:04:05")
-        #   pkt = eth/arp
-        expect_arp_reply = """
-            00010203040501020304050608060001080006040002010203040506c0a80101000102030405c0a80102
-        """.upper().replace(" ", "").replace("\n", "")
-        arp_reply = packet_utils.construct_arp_packet(
-            src_mac="01:02:03:04:05:06",
-            dst_mac="00:01:02:03:04:05",
-            src_ip="192.168.1.1",
-            dst_ip="192.168.1.2",
-            op=packet_utils.ARP_REPLY_OP
-        )
-        asserts.assert_equal(expect_arp_reply, arp_reply)
diff --git a/staticlibs/tests/unit/host/python/run_tests.py b/staticlibs/tests/unit/host/python/run_tests.py
index 498dbaf..fa6a310 100644
--- a/staticlibs/tests/unit/host/python/run_tests.py
+++ b/staticlibs/tests/unit/host/python/run_tests.py
@@ -18,7 +18,6 @@
 from host.python.adb_utils_test import TestAdbUtils
 from host.python.apf_utils_test import TestApfUtils
 from host.python.assert_utils_test import TestAssertUtils
-from host.python.packet_utils_test import TestPacketUtils
 from mobly import suite_runner
 
 
@@ -32,5 +31,5 @@
   sys.argv.pop(1)
   # TODO: make the tests can be executed without manually list classes.
   suite_runner.run_suite(
-      [TestAssertUtils, TestAdbUtils, TestApfUtils, TestPacketUtils], sys.argv
+      [TestAssertUtils, TestAdbUtils, TestApfUtils], sys.argv
   )
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/CarrierConfigRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/CarrierConfigRule.kt
index c9d2527..076398e 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/CarrierConfigRule.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/CarrierConfigRule.kt
@@ -40,6 +40,7 @@
 import com.android.testutils.runAsShell
 import com.android.testutils.tryTest
 import java.security.MessageDigest
+import kotlin.test.assertEquals
 import kotlin.test.assertNotNull
 import kotlin.test.assertTrue
 import org.junit.rules.TestRule
@@ -204,6 +205,7 @@
                 }
                 return@tryTest
             }
+            cv.close()
             if (hold) {
                 addConfigOverrides(subId, PersistableBundle().also {
                     it.putStringArray(CarrierConfigManager.KEY_CARRIER_CERTIFICATE_STRING_ARRAY,
@@ -212,6 +214,9 @@
             } else {
                 cleanUpNow()
             }
+            assertTrue(cv.block(CARRIER_CONFIG_CHANGE_TIMEOUT_MS),
+                "Timed out waiting for CarrierPrivilegesCallback")
+            assertEquals(cpb.hasPrivilege, hold, "Couldn't set carrier privilege")
         } cleanup @JvmSerializableLambda {
             runAsShell(READ_PRIVILEGED_PHONE_STATE) @JvmSerializableLambda {
                 tm.unregisterCarrierPrivilegesCallback(cpb)
diff --git a/staticlibs/testutils/host/python/apf_utils.py b/staticlibs/testutils/host/python/apf_utils.py
index fa8a1da..4835c23 100644
--- a/staticlibs/testutils/host/python/apf_utils.py
+++ b/staticlibs/testutils/host/python/apf_utils.py
@@ -439,10 +439,10 @@
     def wrapper(self, *args, **kwargs):
       asserts.abort_class_if(
         (not hasattr(self, 'client')) or (not hasattr(self.client, 'isAtLeastB')),
-        "client device is not B+"
+        "no valid client attribute"
       )
 
-      asserts.skip_if(not self.client.isAtLeastB(), "not B+")
+      asserts.abort_class_if(not self.client.isAtLeastB(), "client device is not Android B+")
       return test_function(self, *args, **kwargs)
     return wrapper
   return decorator
diff --git a/staticlibs/testutils/host/python/packet_utils.py b/staticlibs/testutils/host/python/packet_utils.py
deleted file mode 100644
index b613f03..0000000
--- a/staticlibs/testutils/host/python/packet_utils.py
+++ /dev/null
@@ -1,70 +0,0 @@
-#  Copyright (C) 2024 The Android Open Source Project
-#
-#  Licensed under the Apache License, Version 2.0 (the "License");
-#  you may not use this file except in compliance with the License.
-#  You may obtain a copy of the License at
-#
-#       http://www.apache.org/licenses/LICENSE-2.0
-#
-#  Unless required by applicable law or agreed to in writing, software
-#  distributed under the License is distributed on an "AS IS" BASIS,
-#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-#  See the License for the specific language governing permissions and
-#  limitations under the License.
-from ipaddress import IPv4Address
-from socket import inet_aton
-
-ETHER_BROADCAST_MAC_ADDRESS = "FF:FF:FF:FF:FF:FF"
-ARP_REQUEST_OP = 1
-ARP_REPLY_OP = 2
-
-"""
-This variable defines a template for constructing ARP packets in hexadecimal format.
-It's used to provide the common fields for ARP packet, and replaced needed fields when constructing
-"""
-ARP_TEMPLATE = (
-    # Ether Header (14 bytes)
-    "{dst_mac}" + # DA
-    "{src_mac}" + # SA
-    "0806" + # ARP
-    # ARP Header (28 bytes)
-    "0001" + # Hardware type (Ethernet)
-    "0800" + # Protocol type (IPv4)
-    "06" + # hardware address length
-    "04" + # protocol address length
-    "{opcode}" + # opcode
-    "{sender_mac}" + # sender MAC
-    "{sender_ip}" + # sender IP
-    "{target_mac}" + # target MAC
-    "{target_ip}" # target IP
-)
-
-def construct_arp_packet(src_mac, dst_mac, src_ip, dst_ip, op) -> str:
-    """Constructs an ARP packet as a hexadecimal string.
-
-    This function creates an ARP packet by filling in the required fields
-    in a predefined ARP packet template.
-
-    Args:
-    src_mac: The MAC address of the sender. (e.g. "11:22:33:44:55:66")
-    dst_mac: The MAC address of the recipient. (e.g. "aa:bb:cc:dd:ee:ff")
-    src_ip: The IP address of the sender. (e.g. "1.1.1.1")
-    dst_ip: The IP address of the target machine. (e.g. "2.2.2.2")
-    op: The op code of the ARP packet, refer to ARP_*_OP
-
-    Returns:
-    A string representing the ARP packet in hexadecimal format.
-    """
-    # Replace the needed fields from packet template
-    arp_pkt = ARP_TEMPLATE.format(
-            dst_mac=dst_mac.replace(":",""),
-            src_mac=src_mac.replace(":",""),
-            opcode=str(op).rjust(4, "0"),
-            sender_mac=src_mac.replace(":",""),
-            sender_ip=inet_aton(src_ip).hex(),
-            target_mac=("000000000000" if op == ARP_REQUEST_OP else dst_mac.replace(":", "")),
-            target_ip=inet_aton(dst_ip).hex()
-    )
-
-    # always convert to upper case hex string
-    return arp_pkt.upper()
\ No newline at end of file
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index d694637..3fc2af0 100644
--- a/tests/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -56,6 +56,7 @@
 import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
 import static android.net.NetworkCapabilities.TRANSPORT_SATELLITE;
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+import static android.net.NetworkCapabilities.TRANSPORT_THREAD;
 import static android.net.NetworkCapabilities.TRANSPORT_USB;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
@@ -1532,4 +1533,93 @@
         nc.setReservationId(43);
         assertNotEquals(nc, other);
     }
+
+    @Test
+    public void testMatchNonThreadLocalNetworks_equals() {
+        final NetworkCapabilities nc = new NetworkCapabilities();
+        nc.setMatchNonThreadLocalNetworks(true);
+        final NetworkCapabilities other = new NetworkCapabilities(nc);
+        assertEquals(nc, other);
+
+        nc.setMatchNonThreadLocalNetworks(false);
+        assertNotEquals(nc, other);
+    }
+
+    @Test
+    public void testMatchNonThreadLocalNetworks_enabled() {
+        doTestMatchNonThreadLocalNetworks(true);
+    }
+
+    @Test
+    public void testMatchNonThreadLocalNetworks_disabled() {
+        doTestMatchNonThreadLocalNetworks(false);
+    }
+
+    private void doTestMatchNonThreadLocalNetworks(boolean enabled) {
+        // Setup request NCs.
+        final NetworkCapabilities noTransportRequestNc = new NetworkCapabilities();
+        final NetworkCapabilities threadRequestNc =
+                new NetworkCapabilities.Builder().addTransportType(TRANSPORT_THREAD).build();
+        final NetworkCapabilities wifiRequestNc =
+                new NetworkCapabilities.Builder().addTransportType(TRANSPORT_WIFI).build();
+        final NetworkCapabilities multiTransportRequestNc =
+                new NetworkCapabilities.Builder().addTransportType(
+                        TRANSPORT_THREAD).addTransportType(TRANSPORT_WIFI).build();
+
+        // Setup network NCs.
+        final NetworkCapabilities localNoTransportNc = new NetworkCapabilities.Builder()
+                .addCapability(NET_CAPABILITY_LOCAL_NETWORK).build();
+        final NetworkCapabilities localThreadsNc = new NetworkCapabilities.Builder()
+                .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                .addTransportType(TRANSPORT_THREAD).build();
+        final NetworkCapabilities localWifiNc = new NetworkCapabilities.Builder()
+                .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                .addTransportType(TRANSPORT_WIFI).build();
+        final NetworkCapabilities wanWifiNc = new NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_WIFI).build();
+
+        // Mark flags accordingly.
+        noTransportRequestNc.setMatchNonThreadLocalNetworks(enabled);
+        threadRequestNc.setMatchNonThreadLocalNetworks(enabled);
+        wifiRequestNc.setMatchNonThreadLocalNetworks(enabled);
+        multiTransportRequestNc.setMatchNonThreadLocalNetworks(enabled);
+
+        if (enabled) {
+            // A request with no specific transport matches all networks.
+            assertTrue(noTransportRequestNc.satisfiedByNetworkCapabilities(localNoTransportNc));
+            assertTrue(noTransportRequestNc.satisfiedByNetworkCapabilities(localWifiNc));
+        } else {
+            // A request with no specific transport only matches thread networks.
+            assertFalse(noTransportRequestNc.satisfiedByNetworkCapabilities(localNoTransportNc));
+            assertFalse(noTransportRequestNc.satisfiedByNetworkCapabilities(localWifiNc));
+        }
+        assertTrue(noTransportRequestNc.satisfiedByNetworkCapabilities(localThreadsNc));
+        assertTrue(noTransportRequestNc.satisfiedByNetworkCapabilities(wanWifiNc));
+
+        // A request with TRANSPORT_THREAD only matches thread networks.
+        assertFalse(threadRequestNc.satisfiedByNetworkCapabilities(localNoTransportNc));
+        assertTrue(threadRequestNc.satisfiedByNetworkCapabilities(localThreadsNc));
+        assertFalse(threadRequestNc.satisfiedByNetworkCapabilities(localWifiNc));
+        assertFalse(threadRequestNc.satisfiedByNetworkCapabilities(wanWifiNc));
+
+        assertFalse(multiTransportRequestNc.satisfiedByNetworkCapabilities(localNoTransportNc));
+        assertTrue(multiTransportRequestNc.satisfiedByNetworkCapabilities(localThreadsNc));
+        assertTrue(multiTransportRequestNc.satisfiedByNetworkCapabilities(wanWifiNc));
+        if (enabled) {
+            assertTrue(multiTransportRequestNc.satisfiedByNetworkCapabilities(localWifiNc));
+        } else {
+            // A request with multiple transports only matches thread networks.
+            assertFalse(multiTransportRequestNc.satisfiedByNetworkCapabilities(localWifiNc));
+        }
+
+        assertFalse(wifiRequestNc.satisfiedByNetworkCapabilities(localNoTransportNc));
+        assertFalse(wifiRequestNc.satisfiedByNetworkCapabilities(localThreadsNc));
+        assertTrue(wifiRequestNc.satisfiedByNetworkCapabilities(wanWifiNc));
+        if (enabled) {
+            assertTrue(wifiRequestNc.satisfiedByNetworkCapabilities(localWifiNc));
+        } else {
+            // A request without TRANSPORT_THREAD matches nothing.
+            assertFalse(wifiRequestNc.satisfiedByNetworkCapabilities(localWifiNc));
+        }
+    }
 }
diff --git a/tests/cts/multidevices/apfv6_test.py b/tests/cts/multidevices/apfv6_test.py
index fb45f4a..2404966 100644
--- a/tests/cts/multidevices/apfv6_test.py
+++ b/tests/cts/multidevices/apfv6_test.py
@@ -14,10 +14,23 @@
 
 from mobly import asserts
 from scapy.layers.inet import IP, ICMP, IPOption_Router_Alert
-from scapy.layers.inet6 import IPv6, ICMPv6EchoRequest, ICMPv6EchoReply
-from scapy.layers.l2 import Ether
+from scapy.layers.inet6 import (
+    IPv6,
+    IPv6ExtHdrHopByHop,
+    ICMPv6EchoRequest,
+    ICMPv6EchoReply,
+    ICMPv6MLQuery2,
+    ICMPv6MLReport2,
+    ICMPv6MLDMultAddrRec,
+    ICMPv6NDOptSrcLLAddr,
+    ICMPv6NDOptDstLLAddr,
+    ICMPv6ND_NS,
+    ICMPv6ND_NA,
+    RouterAlert
+)
+from scapy.layers.l2 import ARP, Ether
 from scapy.contrib.igmpv3 import IGMPv3, IGMPv3mq, IGMPv3mr, IGMPv3gr
-from net_tests_utils.host.python import apf_test_base, apf_utils, adb_utils, assert_utils, packet_utils
+from net_tests_utils.host.python import apf_test_base, apf_utils, adb_utils, assert_utils
 
 APFV6_VERSION = 6000
 ARP_OFFLOAD_REPLY_LEN = 60
@@ -40,51 +53,75 @@
         super().teardown_class()
 
     def test_unicast_arp_request_offload(self):
-        arp_request = packet_utils.construct_arp_packet(
-            src_mac=self.server_mac_address,
-            dst_mac=self.client_mac_address,
-            src_ip=self.server_ipv4_addresses[0],
-            dst_ip=self.client_ipv4_addresses[0],
-            op=packet_utils.ARP_REQUEST_OP
+        eth = Ether(src=self.server_mac_address, dst=self.client_mac_address)
+        arp = ARP(
+            op=1,
+            psrc=self.server_ipv4_addresses[0],
+            pdst=self.client_ipv4_addresses[0],
+            hwsrc=self.server_mac_address
         )
+        arp_request = bytes(eth/arp).hex()
 
-        arp_reply = packet_utils.construct_arp_packet(
-            src_mac=self.client_mac_address,
-            dst_mac=self.server_mac_address,
-            src_ip=self.client_ipv4_addresses[0],
-            dst_ip=self.server_ipv4_addresses[0],
-            op=packet_utils.ARP_REPLY_OP
+        eth = Ether(src=self.client_mac_address, dst=self.server_mac_address)
+        arp = ARP(
+            op=2,
+            psrc=self.client_ipv4_addresses[0],
+            pdst=self.server_ipv4_addresses[0],
+            hwsrc=self.client_mac_address,
+            hwdst=self.server_mac_address
         )
+        expected_arp_reply = bytes(eth/arp).hex()
 
         # Add zero padding up to 60 bytes, since APFv6 ARP offload always sent out 60 bytes reply
-        arp_reply = arp_reply.ljust(ARP_OFFLOAD_REPLY_LEN * 2, "0")
+        expected_arp_reply = expected_arp_reply.ljust(ARP_OFFLOAD_REPLY_LEN * 2, "0")
 
         self.send_packet_and_expect_reply_received(
-            arp_request, "DROPPED_ARP_REQUEST_REPLIED", arp_reply
+            arp_request, "DROPPED_ARP_REQUEST_REPLIED", expected_arp_reply
         )
 
     def test_broadcast_arp_request_offload(self):
-        arp_request = packet_utils.construct_arp_packet(
-            src_mac=self.server_mac_address,
-            dst_mac=packet_utils.ETHER_BROADCAST_MAC_ADDRESS,
-            src_ip=self.server_ipv4_addresses[0],
-            dst_ip=self.client_ipv4_addresses[0],
-            op=packet_utils.ARP_REQUEST_OP
+        eth = Ether(src=self.server_mac_address, dst='ff:ff:ff:ff:ff:ff')
+        arp = ARP(
+            op=1,
+            psrc=self.server_ipv4_addresses[0],
+            pdst=self.client_ipv4_addresses[0],
+            hwsrc=self.server_mac_address
         )
+        arp_request = bytes(eth/arp).hex()
 
-        arp_reply = packet_utils.construct_arp_packet(
-            src_mac=self.client_mac_address,
-            dst_mac=self.server_mac_address,
-            src_ip=self.client_ipv4_addresses[0],
-            dst_ip=self.server_ipv4_addresses[0],
-            op=packet_utils.ARP_REPLY_OP
+        eth = Ether(src=self.client_mac_address, dst=self.server_mac_address)
+        arp = ARP(
+            op=2,
+            psrc=self.client_ipv4_addresses[0],
+            pdst=self.server_ipv4_addresses[0],
+            hwsrc=self.client_mac_address,
+            hwdst=self.server_mac_address
         )
+        expected_arp_reply = bytes(eth/arp).hex()
 
         # Add zero padding up to 60 bytes, since APFv6 ARP offload always sent out 60 bytes reply
-        arp_reply = arp_reply.ljust(ARP_OFFLOAD_REPLY_LEN * 2, "0")
+        expected_arp_reply = expected_arp_reply.ljust(ARP_OFFLOAD_REPLY_LEN * 2, "0")
 
         self.send_packet_and_expect_reply_received(
-            arp_request, "DROPPED_ARP_REQUEST_REPLIED", arp_reply
+            arp_request, "DROPPED_ARP_REQUEST_REPLIED", expected_arp_reply
+        )
+
+    def test_non_dad_ipv6_neighbor_solicitation_offload(self):
+        eth = Ether(src=self.server_mac_address, dst=self.client_mac_address)
+        ip = IPv6(src=self.server_ipv6_addresses[0], dst=self.client_ipv6_addresses[0])
+        icmpv6 = ICMPv6ND_NS(tgt=self.client_ipv6_addresses[0])
+        opt = ICMPv6NDOptSrcLLAddr(lladdr=self.server_mac_address)
+        neighbor_solicitation = bytes(eth/ip/icmpv6/opt).hex()
+
+        eth = Ether(src=self.client_mac_address, dst=self.server_mac_address)
+        ip = IPv6(src=self.client_ipv6_addresses[0], dst=self.server_ipv6_addresses[0])
+        icmpv6 = ICMPv6ND_NA(tgt=self.client_ipv6_addresses[0], R=1, S=1, O=1)
+        opt = ICMPv6NDOptDstLLAddr(lladdr=self.client_mac_address)
+        expected_neighbor_advertisement = bytes(eth/ip/icmpv6/opt).hex()
+        self.send_packet_and_expect_reply_received(
+            neighbor_solicitation,
+            "DROPPED_IPV6_NS_REPLIED_NON_DAD",
+            expected_neighbor_advertisement
         )
 
     @apf_utils.at_least_B()
@@ -103,6 +140,7 @@
         )
 
     @apf_utils.at_least_B()
+    @apf_utils.apf_ram_at_least(3000)
     def test_ipv6_icmp_echo_request_offload(self):
         eth = Ether(src=self.server_mac_address, dst=self.client_mac_address)
         ip = IPv6(src=self.server_ipv6_addresses[0], dst=self.client_ipv6_addresses[0])
@@ -161,3 +199,28 @@
                 self.clientDevice,
                 f'ip addr del {addr}/32 dev {self.client_iface_name}'
             )
+
+    @apf_utils.at_least_B()
+    @apf_utils.apf_ram_at_least(3000)
+    def test_mldv2_general_query_offload(self):
+        ether = Ether(src=self.server_mac_address, dst='33:33:00:00:00:01')
+        ip = IPv6(src=self.server_ipv6_addresses[0], dst='ff02::1', hlim=1)
+        hopOpts = IPv6ExtHdrHopByHop(options=[RouterAlert(otype=5)])
+        mld = ICMPv6MLQuery2()
+        mldv2_general_query = bytes(ether/ip/hopOpts/mld).hex()
+
+        ether = Ether(src=self.client_mac_address, dst='33:33:00:00:00:16')
+        ip = IPv6(src=self.client_ipv6_addresses[0], dst='ff02::16', hlim=1)
+
+        mcast_addrs = apf_utils.get_exclude_all_host_ipv6_multicast_addresses(
+            self.clientDevice, self.client_iface_name
+        )
+
+        mld_records = []
+        for addr in mcast_addrs:
+            mld_records.append(ICMPv6MLDMultAddrRec(dst=addr, rtype=2))
+        mld = ICMPv6MLReport2(records=mld_records)
+        expected_mldv2_report = bytes(ether/ip/hopOpts/mld).hex()
+        self.send_packet_and_expect_reply_received(
+            mldv2_general_query, "DROPPED_IPV6_MLD_V2_GENERAL_QUERY_REPLIED", expected_mldv2_report
+        )
diff --git a/tests/cts/net/AndroidManifest.xml b/tests/cts/net/AndroidManifest.xml
index 098cc0a..acf89be 100644
--- a/tests/cts/net/AndroidManifest.xml
+++ b/tests/cts/net/AndroidManifest.xml
@@ -26,6 +26,7 @@
     <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
     <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
     <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+    <uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
     <uses-permission android:name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS" />
     <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
diff --git a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
index df4dab5..d531e7a 100644
--- a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
+++ b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
@@ -774,7 +774,9 @@
         runAsShell(MANAGE_TEST_NETWORKS) { agent.register() }
         // Without the fix, this will crash the system with SIGSEGV.
         agent.sendAddDscpPolicy(DscpPolicy.Builder(1, 1).build())
-        agent.expectCallback<OnDscpPolicyStatusUpdated>()
+        // Will receive OnNetworkCreated first if the agent is created early. To avoid reading
+        // the flag here, use eventuallyExpect.
+        agent.eventuallyExpect<OnDscpPolicyStatusUpdated>()
     }
 }
 
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index bd9bd2a..4703ac7 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -15,6 +15,7 @@
  */
 package android.net.cts
 
+import android.Manifest.permission.NEARBY_WIFI_DEVICES
 import android.Manifest.permission.NETWORK_SETTINGS
 import android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE
 import android.app.Instrumentation
@@ -179,6 +180,10 @@
 // without affecting the run time of successful runs. Thus, set a very high timeout.
 private const val DEFAULT_TIMEOUT_MS = 5000L
 
+private const val QUEUE_NETWORK_AGENT_EVENTS_IN_SYSTEM_SERVER =
+    "queue_network_agent_events_in_system_server"
+
+
 // When waiting for a NetworkCallback to determine there was no timeout, waiting is the
 // only possible thing (the relevant handler is the one in the real ConnectivityService,
 // and then there is the Binder call), so have a short timeout for this as it will be
@@ -203,12 +208,6 @@
 private val PREFIX = IpPrefix("2001:db8::/64")
 private val NEXTHOP = InetAddresses.parseNumericAddress("fe80::abcd")
 
-// On T and below, the native network is only created when the agent connects.
-// Starting in U, the native network was to be created as soon as the agent is registered,
-// but this has been flagged off for now pending resolution of race conditions.
-// TODO : enable this in a Mainline update or in V.
-private const val SHOULD_CREATE_NETWORKS_IMMEDIATELY = false
-
 @AppModeFull(reason = "Instant apps can't use NetworkAgent because it needs NETWORK_FACTORY'.")
 // NetworkAgent is updated as part of the connectivity module, and running NetworkAgent tests in MTS
 // for modules other than Connectivity does not provide much value. Only run them in connectivity
@@ -234,9 +233,27 @@
     private var qosTestSocket: Closeable? = null // either Socket or DatagramSocket
     private val ifacesToCleanUp = mutableListOf<TestNetworkInterface>()
 
+    // Unless the queuing in system server feature is chickened out, native networks are created
+    // immediately. Historically they would only created as they'd connect, which would force
+    // the code to apply link properties multiple times and suffer errors early on. Creating
+    // them early required that ordering between the client and the system server is guaranteed
+    // (at least to some extent), which has been done by moving the event queue from the client
+    // to the system server. When that feature is not chickened out, create networks immediately.
+    private val SHOULD_CREATE_NETWORKS_IMMEDIATELY
+        get() = mCM.isConnectivityServiceFeatureEnabledForTesting(
+            QUEUE_NETWORK_AGENT_EVENTS_IN_SYSTEM_SERVER
+        )
+
+
     @Before
     fun setUp() {
         instrumentation.getUiAutomation().adoptShellPermissionIdentity()
+        if (SdkLevel.isAtLeastT()) {
+            instrumentation.getUiAutomation().grantRuntimePermission(
+                "android.net.cts",
+                NEARBY_WIFI_DEVICES
+            )
+        }
         mHandlerThread.start()
     }
 
@@ -741,12 +758,24 @@
         tryTest {
             // This process is not the carrier service UID, so allowedUids should be ignored in all
             // the following cases.
-            doTestAllowedUidsWithSubId(defaultSubId, TRANSPORT_CELLULAR, uid,
-                    expectUidsPresent = false)
-            doTestAllowedUidsWithSubId(defaultSubId, TRANSPORT_WIFI, uid,
-                    expectUidsPresent = false)
-            doTestAllowedUidsWithSubId(defaultSubId, TRANSPORT_BLUETOOTH, uid,
-                    expectUidsPresent = false)
+            doTestAllowedUidsWithSubId(
+                defaultSubId,
+                TRANSPORT_CELLULAR,
+                uid,
+                    expectUidsPresent = false
+            )
+            doTestAllowedUidsWithSubId(
+                defaultSubId,
+                TRANSPORT_WIFI,
+                uid,
+                    expectUidsPresent = false
+            )
+            doTestAllowedUidsWithSubId(
+                defaultSubId,
+                TRANSPORT_BLUETOOTH,
+                uid,
+                    expectUidsPresent = false
+            )
 
             // The tools to set the carrier service package override do not exist before U,
             // so there is no way to test the rest of this test on < U.
@@ -764,9 +793,11 @@
             val timeout = SystemClock.elapsedRealtime() + DEFAULT_TIMEOUT_MS
             while (true) {
                 if (SystemClock.elapsedRealtime() > timeout) {
-                    fail("Couldn't make $servicePackage the service package for $defaultSubId: " +
+                    fail(
+                        "Couldn't make $servicePackage the service package for $defaultSubId: " +
                             "dumpsys connectivity".execute().split("\n")
-                                    .filter { it.contains("Logical slot = $defaultSlotIndex.*") })
+                                    .filter { it.contains("Logical slot = $defaultSlotIndex.*") }
+                    )
                 }
                 if ("dumpsys connectivity"
                         .execute()
@@ -789,10 +820,18 @@
                 // TODO(b/315136340): Allow ownerUid to see allowedUids and enable below test case
                 // doTestAllowedUids(defaultSubId, TRANSPORT_WIFI, uid, expectUidsPresent = true)
             }
-            doTestAllowedUidsWithSubId(defaultSubId, TRANSPORT_BLUETOOTH, uid,
-                    expectUidsPresent = false)
-            doTestAllowedUidsWithSubId(defaultSubId, intArrayOf(TRANSPORT_CELLULAR, TRANSPORT_WIFI),
-                    uid, expectUidsPresent = false)
+            doTestAllowedUidsWithSubId(
+                defaultSubId,
+                TRANSPORT_BLUETOOTH,
+                uid,
+                    expectUidsPresent = false
+            )
+            doTestAllowedUidsWithSubId(
+                defaultSubId,
+                intArrayOf(TRANSPORT_CELLULAR, TRANSPORT_WIFI),
+                    uid,
+                expectUidsPresent = false
+            )
         }
     }
 
@@ -1660,16 +1699,17 @@
 
         // Connect a third network. Because network1 is awaiting replacement, network3 is preferred
         // as soon as it validates (until then, it is outscored by network1).
-        // The fact that the first events seen by matchAllCallback is the connection of network3
+        // The fact that the first event seen by matchAllCallback is the connection of network3
         // implicitly ensures that no callbacks are sent since network1 was lost.
         val (agent3, network3) = connectNetwork(lp = lp)
-        matchAllCallback.expectAvailableThenValidatedCallbacks(network3)
-        testCallback.expectAvailableDoubleValidatedCallbacks(network3)
-        sendAndExpectUdpPacket(network3, reader, iface)
 
         // As soon as the replacement arrives, network1 is disconnected.
         // Check that this happens before the replacement timeout (5 seconds) fires.
+        matchAllCallback.expectAvailableCallbacks(network3, validated = false)
         matchAllCallback.expect<Lost>(network1, 2_000 /* timeoutMs */)
+        matchAllCallback.expectCaps(network3) { it.hasCapability(NET_CAPABILITY_VALIDATED) }
+        sendAndExpectUdpPacket(network3, reader, iface)
+        testCallback.expectAvailableDoubleValidatedCallbacks(network3)
         agent1.expectCallback<OnNetworkUnwanted>()
 
         // Test lingering:
@@ -1717,7 +1757,7 @@
         val callback = TestableNetworkCallback()
         requestNetwork(makeTestNetworkRequest(specifier = specifier6), callback)
         val agent6 = createNetworkAgent(specifier = specifier6)
-        val network6 = agent6.register()
+        agent6.register()
         if (SHOULD_CREATE_NETWORKS_IMMEDIATELY) {
             agent6.expectCallback<OnNetworkCreated>()
         } else {
@@ -1787,8 +1827,9 @@
 
         val (newWifiAgent, newWifiNetwork) = connectNetwork(TRANSPORT_WIFI)
         testCallback.expectAvailableCallbacks(newWifiNetwork, validated = true)
-        matchAllCallback.expectAvailableThenValidatedCallbacks(newWifiNetwork)
+        matchAllCallback.expectAvailableCallbacks(newWifiNetwork, validated = false)
         matchAllCallback.expect<Lost>(wifiNetwork)
+        matchAllCallback.expectCaps(newWifiNetwork) { it.hasCapability(NET_CAPABILITY_VALIDATED) }
         wifiAgent.expectCallback<OnNetworkUnwanted>()
         testCallback.expect<CapabilitiesChanged>(newWifiNetwork)
 
@@ -1848,8 +1889,10 @@
                 it.setTransportInfo(VpnTransportInfo(
                     VpnManager.TYPE_VPN_PLATFORM,
                     sessionId,
-                    /*bypassable=*/ false,
-                    /*longLivedTcpConnectionsExpensive=*/ false
+                    /*bypassable=*/
+                    false,
+                    /*longLivedTcpConnectionsExpensive=*/
+                    false
                 ))
                 it.underlyingNetworks = listOf()
             }
diff --git a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
index abe628b..92d58e6 100644
--- a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
+++ b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
@@ -36,6 +36,7 @@
 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_UNKNOWN_REQUEST;
 import static android.net.cts.util.CtsTetheringUtils.isAnyIfaceMatch;
 import static android.os.Process.INVALID_UID;
 
@@ -714,4 +715,185 @@
         assertThrows(UnsupportedOperationException.class, () -> mTM.tether("iface"));
         assertThrows(UnsupportedOperationException.class, () -> mTM.untether("iface"));
     }
+
+    @Test
+    public void testCarrierPrivilegedIsTetheringSupported() throws Exception {
+        assumeTrue(SdkLevel.isAtLeastB());
+        assumeTrue(mPm.hasSystemFeature(FEATURE_TELEPHONY));
+        int defaultSubId = SubscriptionManager.getDefaultSubscriptionId();
+        mCarrierConfigRule.acquireCarrierPrivilege(defaultSubId);
+        final TestTetheringEventCallback tetherEventCallback =
+                mCtsTetheringUtils.registerTetheringEventCallback();
+        try {
+            tetherEventCallback.assumeWifiTetheringSupported(mContext);
+            tetherEventCallback.expectNoTetheringActive();
+
+            assertTrue(mTM.isTetheringSupported());
+        } finally {
+            mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
+        }
+    }
+
+    @Test
+    public void testCarrierPrivilegedStartTetheringNonWifiFails() throws Exception {
+        assumeTrue(SdkLevel.isAtLeastB());
+        assumeTrue(mPm.hasSystemFeature(FEATURE_TELEPHONY));
+        int defaultSubId = SubscriptionManager.getDefaultSubscriptionId();
+        mCarrierConfigRule.acquireCarrierPrivilege(defaultSubId);
+        final TestTetheringEventCallback tetherEventCallback =
+                mCtsTetheringUtils.registerTetheringEventCallback();
+        try {
+            tetherEventCallback.assumeWifiTetheringSupported(mContext);
+            tetherEventCallback.expectNoTetheringActive();
+            StartTetheringCallback callback = new StartTetheringCallback();
+            TetheringRequest request = new TetheringRequest.Builder(TETHERING_USB).build();
+
+            mTM.startTethering(request, Runnable::run, callback);
+
+            callback.expectTetheringFailed(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+        } finally {
+            mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
+        }
+    }
+
+    @Test
+    public void testCarrierPrivilegedStartTetheringWifiWithoutConfigFails() throws Exception {
+        assumeTrue(SdkLevel.isAtLeastB());
+        assumeTrue(mPm.hasSystemFeature(FEATURE_TELEPHONY));
+        int defaultSubId = SubscriptionManager.getDefaultSubscriptionId();
+        mCarrierConfigRule.acquireCarrierPrivilege(defaultSubId);
+        final TestTetheringEventCallback tetherEventCallback =
+                mCtsTetheringUtils.registerTetheringEventCallback();
+        try {
+            tetherEventCallback.assumeWifiTetheringSupported(mContext);
+            tetherEventCallback.expectNoTetheringActive();
+            StartTetheringCallback callback = new StartTetheringCallback();
+            TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build();
+
+            mTM.startTethering(request, Runnable::run, callback);
+
+            callback.expectTetheringFailed(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+        } finally {
+            mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
+        }
+    }
+
+    @Test
+    public void testCarrierPrivilegedStartTetheringWifiWithConfigSucceeds() throws Exception {
+        assumeTrue(SdkLevel.isAtLeastB());
+        assumeTrue(mPm.hasSystemFeature(FEATURE_TELEPHONY));
+        int defaultSubId = SubscriptionManager.getDefaultSubscriptionId();
+        mCarrierConfigRule.acquireCarrierPrivilege(defaultSubId);
+        final TestTetheringEventCallback tetherEventCallback =
+                mCtsTetheringUtils.registerTetheringEventCallback();
+        try {
+            tetherEventCallback.assumeWifiTetheringSupported(mContext);
+            tetherEventCallback.expectNoTetheringActive();
+            SoftApConfiguration softApConfig = createSoftApConfiguration("Carrier-privileged");
+
+            mCtsTetheringUtils.startWifiTetheringNoPermissions(tetherEventCallback, softApConfig);
+        } finally {
+            mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
+        }
+    }
+
+    @Test
+    public void testCarrierPrivilegedStopTetheringNonWifiFails() throws Exception {
+        assumeTrue(SdkLevel.isAtLeastB());
+        assumeTrue(mPm.hasSystemFeature(FEATURE_TELEPHONY));
+        int defaultSubId = SubscriptionManager.getDefaultSubscriptionId();
+        mCarrierConfigRule.acquireCarrierPrivilege(defaultSubId);
+        final TestTetheringEventCallback tetherEventCallback =
+                mCtsTetheringUtils.registerTetheringEventCallback();
+        try {
+            tetherEventCallback.assumeWifiTetheringSupported(mContext);
+            tetherEventCallback.expectNoTetheringActive();
+            TetheringRequest request = new TetheringRequest.Builder(TETHERING_USB).build();
+            CtsTetheringUtils.StopTetheringCallback
+                    callback = new CtsTetheringUtils.StopTetheringCallback();
+
+            mTM.stopTethering(request, Runnable::run, callback);
+
+            callback.expectStopTetheringFailed(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+        } finally {
+            mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
+        }
+    }
+
+    @Test
+    public void testCarrierPrivilegedStopTetheringWifiWithoutConfigFails() throws Exception {
+        assumeTrue(SdkLevel.isAtLeastB());
+        assumeTrue(mPm.hasSystemFeature(FEATURE_TELEPHONY));
+        int defaultSubId = SubscriptionManager.getDefaultSubscriptionId();
+        mCarrierConfigRule.acquireCarrierPrivilege(defaultSubId);
+        final TestTetheringEventCallback tetherEventCallback =
+                mCtsTetheringUtils.registerTetheringEventCallback();
+        try {
+            tetherEventCallback.assumeWifiTetheringSupported(mContext);
+            tetherEventCallback.expectNoTetheringActive();
+            TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build();
+            CtsTetheringUtils.StopTetheringCallback
+                    callback = new CtsTetheringUtils.StopTetheringCallback();
+
+            mTM.stopTethering(request, Runnable::run, callback);
+
+            callback.expectStopTetheringFailed(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+        } finally {
+            mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
+        }
+    }
+
+    @Test
+    public void testCarrierPrivilegedStopTetheringWifiWithConfigButNoActiveRequestFails()
+            throws Exception {
+        assumeTrue(SdkLevel.isAtLeastB());
+        assumeTrue(mPm.hasSystemFeature(FEATURE_TELEPHONY));
+        int defaultSubId = SubscriptionManager.getDefaultSubscriptionId();
+        mCarrierConfigRule.acquireCarrierPrivilege(defaultSubId);
+        final TestTetheringEventCallback tetherEventCallback =
+                mCtsTetheringUtils.registerTetheringEventCallback();
+        try {
+            tetherEventCallback.assumeWifiTetheringSupported(mContext);
+            tetherEventCallback.expectNoTetheringActive();
+            SoftApConfiguration softApConfig = createSoftApConfiguration("Carrier-privileged");
+            TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI)
+                    .setSoftApConfiguration(softApConfig)
+                    .build();
+            CtsTetheringUtils.StopTetheringCallback
+                    callback = new CtsTetheringUtils.StopTetheringCallback();
+
+            mTM.stopTethering(request, Runnable::run, callback);
+
+            callback.expectStopTetheringFailed(TETHER_ERROR_UNKNOWN_REQUEST);
+        } finally {
+            mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
+        }
+    }
+
+    @Test
+    public void testCarrierPrivilegedStopTetheringWifiWithConfigSucceeds() throws Exception {
+        assumeTrue(SdkLevel.isAtLeastB());
+        assumeTrue(mPm.hasSystemFeature(FEATURE_TELEPHONY));
+        int defaultSubId = SubscriptionManager.getDefaultSubscriptionId();
+        mCarrierConfigRule.acquireCarrierPrivilege(defaultSubId);
+        final TestTetheringEventCallback tetherEventCallback =
+                mCtsTetheringUtils.registerTetheringEventCallback();
+        try {
+            tetherEventCallback.assumeWifiTetheringSupported(mContext);
+            tetherEventCallback.expectNoTetheringActive();
+            SoftApConfiguration softApConfig = createSoftApConfiguration("Carrier-privileged");
+            mCtsTetheringUtils.startWifiTetheringNoPermissions(tetherEventCallback, softApConfig);
+            TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI)
+                    .setSoftApConfiguration(softApConfig)
+                    .build();
+            CtsTetheringUtils.StopTetheringCallback
+                    callback = new CtsTetheringUtils.StopTetheringCallback();
+
+            mTM.stopTethering(request, Runnable::run, callback);
+
+            callback.verifyStopTetheringSucceeded();
+        } finally {
+            mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
+        }
+    }
 }
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 3eefa0f..35a7fbd 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -3092,23 +3092,24 @@
         generalCb.expectAvailableCallbacksUnvalidated(net2);
         if (expectLingering) {
             generalCb.expectLosing(net1);
-        }
-        generalCb.expectCaps(net2, c -> c.hasCapability(NET_CAPABILITY_VALIDATED));
-        defaultCb.expectAvailableDoubleValidatedCallbacks(net2);
 
-        // Make sure cell 1 is unwanted immediately if the radio can't time share, but only
-        // after some delay if it can.
-        if (expectLingering) {
+            // Make sure cell 1 is unwanted immediately if the radio can't time share, but only
+            // after some delay if it can.
+            generalCb.expectCaps(net2, c -> c.hasCapability(NET_CAPABILITY_VALIDATED));
+            defaultCb.expectAvailableDoubleValidatedCallbacks(net2);
             net1.assertNotDisconnected(TEST_CALLBACK_TIMEOUT_MS); // always incurs the timeout
             generalCb.assertNoCallback();
             // assertNotDisconnected waited for TEST_CALLBACK_TIMEOUT_MS, so waiting for the
             // linger period gives TEST_CALLBACK_TIMEOUT_MS time for the event to process.
             net1.expectDisconnected(UNREASONABLY_LONG_ALARM_WAIT_MS);
+            generalCb.expect(LOST, net1);
         } else {
             net1.expectDisconnected(TEST_CALLBACK_TIMEOUT_MS);
+            net1.disconnect();
+            generalCb.expect(LOST, net1);
+            generalCb.expectCaps(net2, c -> c.hasCapability(NET_CAPABILITY_VALIDATED));
+            defaultCb.expectAvailableDoubleValidatedCallbacks(net2);
         }
-        net1.disconnect();
-        generalCb.expect(LOST, net1);
 
         // Remove primary from net 2
         net2.setScore(new NetworkScore.Builder().build());
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSL2capProviderTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSL2capProviderTest.kt
index babcba9..ee5b4ee 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSL2capProviderTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSL2capProviderTest.kt
@@ -38,12 +38,14 @@
 import android.net.NetworkSpecifier
 import android.net.RouteInfo
 import android.os.Build
+import android.os.Handler
 import android.os.HandlerThread
 import android.os.ParcelFileDescriptor
 import com.android.server.net.L2capNetwork.L2capIpClient
 import com.android.server.net.L2capPacketForwarder
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
 import com.android.testutils.RecorderCallback.CallbackEntry.Reserved
 import com.android.testutils.RecorderCallback.CallbackEntry.Unavailable
 import com.android.testutils.TestableNetworkCallback
@@ -59,6 +61,7 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers.eq
 import org.mockito.ArgumentMatchers.isNull
 import org.mockito.Mockito.doAnswer
@@ -394,4 +397,34 @@
         val cb2 = requestNetwork(nr)
         cb2.expectAvailableCallbacks(anyNetwork(), validated = false)
     }
+
+    /** Test to ensure onLost() is sent before onUnavailable() when the network is torn down. */
+    @Test
+    fun testClientNetwork_gracefulTearDown() {
+        val specifier = L2capNetworkSpecifier.Builder()
+            .setRole(ROLE_CLIENT)
+            .setHeaderCompression(HEADER_COMPRESSION_NONE)
+            .setRemoteAddress(MacAddress.fromBytes(REMOTE_MAC))
+            .setPsm(PSM)
+            .build()
+
+        val nr = REQUEST.copyWithSpecifier(specifier)
+        val cb = requestNetwork(nr)
+        cb.expectAvailableCallbacks(anyNetwork(), validated = false)
+
+        // Capture the L2capPacketForwarder callback object to tear down the network.
+        val handlerCaptor = ArgumentCaptor.forClass(Handler::class.java)
+        val forwarderCbCaptor = ArgumentCaptor.forClass(L2capPacketForwarder.ICallback::class.java)
+        verify(providerDeps).createL2capPacketForwarder(
+                handlerCaptor.capture(), any(), any(), any(), forwarderCbCaptor.capture())
+        val handler = handlerCaptor.value
+        val forwarderCb = forwarderCbCaptor.value
+
+        // Trigger a forwarding error
+        handler.post { forwarderCb.onError() }
+        handler.waitForIdle(HANDLER_TIMEOUT_MS)
+
+        cb.expect<Lost>()
+        cb.expect<Unavailable>()
+    }
 }
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
index 63daebc..89acf69 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
@@ -20,6 +20,7 @@
 import static android.net.NetworkStats.DEFAULT_NETWORK_NO;
 import static android.net.NetworkStats.METERED_ALL;
 import static android.net.NetworkStats.METERED_NO;
+import static android.net.NetworkStats.METERED_YES;
 import static android.net.NetworkStats.ROAMING_ALL;
 import static android.net.NetworkStats.ROAMING_NO;
 import static android.net.NetworkStats.SET_ALL;
@@ -29,6 +30,8 @@
 import static android.net.NetworkStats.TAG_NONE;
 import static android.net.NetworkStats.UID_ALL;
 
+import static com.android.server.net.NetworkStatsFactory.CONFIG_PER_UID_TAG_THROTTLING;
+import static com.android.server.net.NetworkStatsFactory.CONFIG_PER_UID_TAG_THROTTLING_THRESHOLD;
 import static com.android.server.net.NetworkStatsFactory.kernelToTag;
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
@@ -36,6 +39,9 @@
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 
 import android.content.Context;
@@ -52,12 +58,15 @@
 import com.android.server.BpfNetMaps;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.com.android.testutils.SetFeatureFlagsRule;
+import com.android.testutils.com.android.testutils.SetFeatureFlagsRule.FeatureFlag;
 
 import libcore.io.IoUtils;
 import libcore.testing.io.TestIoUtils;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -66,6 +75,7 @@
 import java.io.File;
 import java.io.IOException;
 import java.net.ProtocolException;
+import java.util.HashMap;
 
 /** Tests for {@link NetworkStatsFactory}. */
 @RunWith(DevSdkIgnoreRunner.class)
@@ -73,6 +83,7 @@
 @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
 public class NetworkStatsFactoryTest extends NetworkStatsBaseTest {
     private static final String CLAT_PREFIX = "v4-";
+    private static final int TEST_TAGS_PER_UID_THRESHOLD = 10;
 
     private File mTestProc;
     private NetworkStatsFactory mFactory;
@@ -80,6 +91,16 @@
     @Mock private NetworkStatsFactory.Dependencies mDeps;
     @Mock private BpfNetMaps mBpfNetMaps;
 
+    final HashMap<String, Boolean> mFeatureFlags = new HashMap<>();
+    // This will set feature flags from @FeatureFlag annotations
+    // into the map before setUp() runs.
+    @Rule
+    public final SetFeatureFlagsRule mSetFeatureFlagsRule =
+            new SetFeatureFlagsRule((name, enabled) -> {
+                mFeatureFlags.put(name, enabled);
+                return null;
+            }, (name) -> mFeatureFlags.getOrDefault(name, false));
+
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
@@ -90,6 +111,10 @@
         // related to networkStatsFactory is compiled to a minimal native library and loaded here.
         System.loadLibrary("networkstatsfactorytestjni");
         doReturn(mBpfNetMaps).when(mDeps).createBpfNetMaps(any());
+        doAnswer(invocation -> mFeatureFlags.getOrDefault((String) invocation.getArgument(1), true))
+            .when(mDeps).isFeatureNotChickenedOut(any(), anyString());
+        doReturn(TEST_TAGS_PER_UID_THRESHOLD).when(mDeps)
+                .getDeviceConfigPropertyInt(eq(CONFIG_PER_UID_TAG_THROTTLING_THRESHOLD), anyInt());
 
         mFactory = new NetworkStatsFactory(mContext, mDeps);
         mFactory.updateUnderlyingNetworkInfos(new UnderlyingNetworkInfo[0]);
@@ -498,6 +523,71 @@
         assertValues(removedUidsStats, TEST_IFACE, UID_GREEN, 64L, 3L, 1024L, 8L);
     }
 
+    @FeatureFlag(name = CONFIG_PER_UID_TAG_THROTTLING)
+    @Test
+    public void testFilterTooManyTags_featureEnabled() throws Exception {
+        doTestFilterTooManyTags(true);
+    }
+
+    @FeatureFlag(name = CONFIG_PER_UID_TAG_THROTTLING, enabled = false)
+    @Test
+    public void testFilterTooManyTags_featureDisabled() throws Exception {
+        doTestFilterTooManyTags(false);
+    }
+
+    private void doTestFilterTooManyTags(boolean supportPerUidTagThrottling) throws Exception {
+        // Add entries for UID_RED which reaches the threshold.
+        final NetworkStats statsWithManyTags = new NetworkStats(0L, TEST_TAGS_PER_UID_THRESHOLD);
+        for (int tag = 1; tag <= TEST_TAGS_PER_UID_THRESHOLD; tag++) {
+            statsWithManyTags.combineValues(
+                    new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, tag,
+                            METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 12L, 18L, 14L, 1L, 0L));
+        }
+        doReturn(statsWithManyTags).when(mDeps).getNetworkStatsDetail();
+        final NetworkStats stats1 = mFactory.readNetworkStatsDetail();
+        assertEquals(stats1.size(), TEST_TAGS_PER_UID_THRESHOLD);
+
+        // Add 2 new entries with pre-existing tag, verify they can be added no matter what.
+        final NetworkStats newDiffWithExistingTag = new NetworkStats(0L, 2);
+        // This one should be added as a new entry, as the metered data doesn't exist yet.
+        newDiffWithExistingTag.combineValues(
+                new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT,
+                        TEST_TAGS_PER_UID_THRESHOLD,
+                        METERED_YES, ROAMING_NO, DEFAULT_NETWORK_NO, 3L, 5L, 8L, 1L, 1L));
+        // This one should be combined into existing entry.
+        newDiffWithExistingTag.combineValues(
+                new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT,
+                        TEST_TAGS_PER_UID_THRESHOLD,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1L, 2L, 3L, 4L, 5L));
+
+        doReturn(newDiffWithExistingTag).when(mDeps).getNetworkStatsDetail();
+        final NetworkStats stats2 = mFactory.readNetworkStatsDetail();
+        assertEquals(stats2.size(), TEST_TAGS_PER_UID_THRESHOLD + 1);
+        assertValues(stats2, TEST_IFACE, UID_RED, SET_DEFAULT, TEST_TAGS_PER_UID_THRESHOLD,
+                METERED_YES, ROAMING_NO, DEFAULT_NETWORK_NO, 3L, 5L, 8L, 1L, 1L);
+        assertValues(stats2, TEST_IFACE, UID_RED, SET_DEFAULT, TEST_TAGS_PER_UID_THRESHOLD,
+                METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 13L, 20L, 17L, 5L, 5L);
+
+        // Add an entry which exceeds the threshold, verify the entry is filtered out.
+        final NetworkStats newDiffWithNonExistingTag = new NetworkStats(0L, 1);
+        newDiffWithNonExistingTag.combineValues(
+                new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT,
+                        TEST_TAGS_PER_UID_THRESHOLD + 1,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 12L, 18L, 14L, 1L, 0L));
+        doReturn(newDiffWithNonExistingTag).when(mDeps).getNetworkStatsDetail();
+        final NetworkStats stats3 = mFactory.readNetworkStatsDetail();
+        if (supportPerUidTagThrottling) {
+            assertEquals(stats3.size(), TEST_TAGS_PER_UID_THRESHOLD + 1);
+            assertNoStatsEntry(stats3, TEST_IFACE, UID_RED, SET_DEFAULT,
+                    TEST_TAGS_PER_UID_THRESHOLD + 1);
+        } else {
+            assertEquals(stats3.size(), TEST_TAGS_PER_UID_THRESHOLD + 2);
+            assertValues(stats3, TEST_IFACE, UID_RED, SET_DEFAULT,
+                    TEST_TAGS_PER_UID_THRESHOLD + 1,
+                    METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 12L, 18L, 14L, 1L, 0L);
+        }
+    }
+
     private NetworkStats buildEmptyStats() {
         return new NetworkStats(SystemClock.elapsedRealtime(), 0);
     }
diff --git a/thread/tests/integration/AndroidManifest.xml b/thread/tests/integration/AndroidManifest.xml
index a049184..8bee1e1 100644
--- a/thread/tests/integration/AndroidManifest.xml
+++ b/thread/tests/integration/AndroidManifest.xml
@@ -24,6 +24,7 @@
     <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
     <uses-permission android:name="android.permission.THREAD_NETWORK_PRIVILEGED"/>
     <uses-permission android:name="android.permission.NETWORK_SETTINGS"/>
+    <uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
     <uses-permission android:name="android.permission.INTERNET"/>
 
     <application android:debuggable="true">
diff --git a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
index c56db02..f586f6e 100644
--- a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
+++ b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
@@ -29,6 +29,8 @@
 import static android.net.thread.utils.ThreadNetworkControllerWrapper.JOIN_TIMEOUT;
 import static android.os.SystemClock.elapsedRealtime;
 
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
 import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
 import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
 import static com.android.testutils.TestPermissionUtil.runAsShell;
@@ -123,6 +125,12 @@
 
     @Before
     public void setUp() throws Exception {
+        getInstrumentation()
+                .getUiAutomation()
+                .grantRuntimePermission(
+                        "com.android.thread.tests.integration",
+                        "android.permission.NEARBY_WIFI_DEVICES");
+
         mExecutor = Executors.newSingleThreadExecutor();
         mFtd = new FullThreadDevice(10 /* nodeId */);
         mOtCtl = new OtDaemonController();