Merge "NetBpfLoad: 25Q2+ parse & verify config" into main
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index 70a3442..a651b1b 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -31,6 +31,7 @@
 import static android.net.TetheringManager.TETHER_ERROR_IFACE_CFG_ERROR;
 import static android.net.TetheringManager.TETHER_ERROR_INTERNAL_ERROR;
 import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_SERVICE_UNAVAIL;
 import static android.net.TetheringManager.TETHER_ERROR_TETHER_IFACE_ERROR;
 import static android.net.TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
 import static android.net.TetheringManager.TetheringRequest.checkStaticAddressConfiguration;
@@ -40,19 +41,25 @@
 
 import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
 import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH;
+import static com.android.networkstack.tethering.TetheringConfiguration.TETHERING_LOCAL_NETWORK_AGENT;
 import static com.android.networkstack.tethering.TetheringConfiguration.USE_SYNC_SM;
 import static com.android.networkstack.tethering.util.PrefixUtils.asIpPrefix;
 import static com.android.networkstack.tethering.util.TetheringMessageBase.BASE_IPSERVER;
+import static com.android.networkstack.tethering.util.TetheringUtils.getTransportTypeForTetherableType;
 
+import android.annotation.SuppressLint;
+import android.content.Context;
 import android.net.INetd;
 import android.net.INetworkStackStatusCallback;
 import android.net.IpPrefix;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.MacAddress;
+import android.net.NetworkAgent;
 import android.net.RouteInfo;
 import android.net.TetheredClient;
 import android.net.TetheringManager.TetheringRequest;
+import android.net.connectivity.ConnectivityInternalApiUtil;
 import android.net.dhcp.DhcpLeaseParcelable;
 import android.net.dhcp.DhcpServerCallbacks;
 import android.net.dhcp.DhcpServingParamsParcel;
@@ -60,7 +67,9 @@
 import android.net.dhcp.IDhcpEventCallbacks;
 import android.net.dhcp.IDhcpServer;
 import android.net.ip.RouterAdvertisementDaemon.RaParams;
+import android.os.Build;
 import android.os.Handler;
+import android.os.Looper;
 import android.os.Message;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
@@ -70,11 +79,13 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.MessageUtils;
 import com.android.internal.util.State;
 import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.DeviceConfigUtils;
 import com.android.net.module.util.IIpv4PrefixRequest;
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.NetdUtils;
@@ -204,6 +215,23 @@
         /** Create a DhcpServer instance to be used by IpServer. */
         public abstract void makeDhcpServer(String ifName, DhcpServingParamsParcel params,
                 DhcpServerCallbacks cb);
+
+        /**
+         * @see DeviceConfigUtils#isTetheringFeatureEnabled
+         */
+        public boolean isFeatureEnabled(Context context, String name) {
+            return DeviceConfigUtils.isTetheringFeatureEnabled(context, name);
+        }
+
+        /** Create a NetworkAgent instance to be used by IpServer. */
+        @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+        @SuppressLint("NewApi")
+        public NetworkAgent makeNetworkAgent(
+                @NonNull Context context, @NonNull Looper looper, @NonNull String logTag,
+                int interfaceType, @NonNull LinkProperties lp) {
+            return ConnectivityInternalApiUtil.buildTetheringNetworkAgent(
+                    context, looper, logTag, getTransportTypeForTetherableType(interfaceType), lp);
+        }
     }
 
     // request from the user that it wants to tether
@@ -304,16 +332,28 @@
 
     private final TetheringMetrics mTetheringMetrics;
     private final Handler mHandler;
+    private final Context mContext;
+
+    private final boolean mSupportLocalAgent;
+
+    // This will be null if the TetheredState is not entered or feature not supported.
+    // This will be only accessed from the IpServer handler thread.
+    private NetworkAgent mTetheringAgent;
+
+    private static boolean everRegistered(@NonNull NetworkAgent agent) {
+        return agent.getNetwork() != null;
+    }
 
     // TODO: Add a dependency object to pass the data members or variables from the tethering
     // object. It helps to reduce the arguments of the constructor.
     public IpServer(
-            String ifaceName, Handler handler, int interfaceType, SharedLog log,
-            INetd netd, @NonNull BpfCoordinator bpfCoordinator,
+            String ifaceName, @NonNull Context context, Handler handler, int interfaceType,
+            SharedLog log, INetd netd, @NonNull BpfCoordinator bpfCoordinator,
             RoutingCoordinatorManager routingCoordinatorManager, Callback callback,
             TetheringConfiguration config,
             TetheringMetrics tetheringMetrics, Dependencies deps) {
         super(ifaceName, USE_SYNC_SM ? null : handler.getLooper());
+        mContext = Objects.requireNonNull(context);
         mHandler = handler;
         mLog = log.forSubComponent(ifaceName);
         mNetd = netd;
@@ -339,6 +379,10 @@
         mLastError = TETHER_ERROR_NO_ERROR;
         mServingMode = STATE_AVAILABLE;
 
+        // Tethering network agent is supported on V+, and will be rolled out gradually.
+        mSupportLocalAgent = SdkLevel.isAtLeastV()
+                && mDeps.isFeatureEnabled(mContext, TETHERING_LOCAL_NETWORK_AGENT);
+
         mInitialState = new InitialState();
         mLocalHotspotState = new LocalHotspotState();
         mTetheredState = new TetheredState();
@@ -787,6 +831,7 @@
     //
     // TODO: Evaluate using a data structure than is more directly suited to
     // communicating only the relevant information.
+    @SuppressLint("NewApi")
     private void updateUpstreamIPv6LinkProperties(LinkProperties v6only, int ttlAdjustment) {
         if (mRaDaemon == null) return;
 
@@ -847,8 +892,7 @@
     }
 
     private void removeRoutesFromNetwork(int netId, @NonNull final List<RouteInfo> toBeRemoved) {
-        final int removalFailures = NetdUtils.removeRoutesFromNetwork(
-                mNetd, netId, toBeRemoved);
+        final int removalFailures = NetdUtils.removeRoutesFromNetwork(mNetd, netId, toBeRemoved);
         if (removalFailures > 0) {
             mLog.e("Failed to remove " + removalFailures
                     + " IPv6 routes from network " + netId + ".");
@@ -900,7 +944,9 @@
         if (!deprecatedPrefixes.isEmpty()) {
             final List<RouteInfo> routesToBeRemoved =
                     getLocalRoutesFor(mIfaceName, deprecatedPrefixes);
-            removeRoutesFromNetwork(LOCAL_NET_ID, routesToBeRemoved);
+            if (mTetheringAgent == null) {
+                removeRoutesFromNetwork(LOCAL_NET_ID, routesToBeRemoved);
+            }
             for (RouteInfo route : routesToBeRemoved) mLinkProperties.removeRoute(route);
         }
 
@@ -914,7 +960,9 @@
             if (!addedPrefixes.isEmpty()) {
                 final List<RouteInfo> routesToBeAdded =
                         getLocalRoutesFor(mIfaceName, addedPrefixes);
-                addRoutesToNetwork(LOCAL_NET_ID, routesToBeAdded);
+                if (mTetheringAgent == null) {
+                    addRoutesToNetwork(LOCAL_NET_ID, routesToBeAdded);
+                }
                 for (RouteInfo route : routesToBeAdded) mLinkProperties.addRoute(route);
             }
         }
@@ -1112,7 +1160,26 @@
             return CONNECTIVITY_SCOPE_LOCAL;
         }
 
+        @SuppressLint("NewApi")
         private void startServingInterface() {
+            // TODO: Enable Network Agent for Wifi P2P Group Owner mode when Network Agent
+            //  for Group Client mode is supported.
+            if (mSupportLocalAgent && getScope() == CONNECTIVITY_SCOPE_GLOBAL) {
+                try {
+                    mTetheringAgent = mDeps.makeNetworkAgent(mContext, Looper.myLooper(), TAG,
+                            mInterfaceType, mLinkProperties);
+                    // Entering CONNECTING state, the ConnectivityService will create the
+                    // native network.
+                    mTetheringAgent.register();
+                } catch (RuntimeException e) {
+                    mLog.e("Error Creating Local Network", e);
+                    // If an exception occurs during the creation or registration of the
+                    // NetworkAgent, it typically indicates a problem with the system services.
+                    mLastError = TETHER_ERROR_SERVICE_UNAVAIL;
+                    return;
+                }
+            }
+
             if (!startIPv4(getScope())) {
                 mLastError = TETHER_ERROR_IFACE_CFG_ERROR;
                 return;
@@ -1122,14 +1189,16 @@
                 // Enable IPv6, disable accepting RA, etc. See TetherController::tetherInterface()
                 // for more detail.
                 mNetd.tetherInterfaceAdd(mIfaceName);
-                NetdUtils.networkAddInterface(mNetd, LOCAL_NET_ID, mIfaceName,
-                        20 /* maxAttempts */, 50 /* pollingIntervalMs */);
-                // Activate a route to dest and IPv6 link local.
-                NetdUtils.modifyRoute(mNetd, NetdUtils.ModifyOperation.ADD, LOCAL_NET_ID,
-                        new RouteInfo(asIpPrefix(mIpv4Address), null, mIfaceName, RTN_UNICAST));
-                NetdUtils.modifyRoute(mNetd, NetdUtils.ModifyOperation.ADD, LOCAL_NET_ID,
-                        new RouteInfo(new IpPrefix("fe80::/64"), null, mIfaceName,
-                                RTN_UNICAST));
+                if (mTetheringAgent == null) {
+                    NetdUtils.networkAddInterface(mNetd, LOCAL_NET_ID, mIfaceName,
+                            20 /* maxAttempts */, 50 /* pollingIntervalMs */);
+                    // Activate a route to dest and IPv6 link local.
+                    NetdUtils.modifyRoute(mNetd, NetdUtils.ModifyOperation.ADD, LOCAL_NET_ID,
+                            new RouteInfo(asIpPrefix(mIpv4Address), null, mIfaceName, RTN_UNICAST));
+                    NetdUtils.modifyRoute(mNetd, NetdUtils.ModifyOperation.ADD, LOCAL_NET_ID,
+                            new RouteInfo(new IpPrefix("fe80::/64"), null, mIfaceName,
+                                    RTN_UNICAST));
+                }
             } catch (RemoteException | ServiceSpecificException | IllegalStateException e) {
                 mLog.e("Error Tethering", e);
                 mLastError = TETHER_ERROR_TETHER_IFACE_ERROR;
@@ -1141,9 +1210,17 @@
                 // TODO: Make this a fatal error once Bluetooth IPv6 is sorted.
                 return;
             }
+
+            if (mTetheringAgent != null && everRegistered(mTetheringAgent)) {
+                mTetheringAgent.sendLinkProperties(mLinkProperties);
+                // Mark it connected to notify the applications for
+                // the network availability.
+                mTetheringAgent.markConnected();
+            }
         }
 
         @Override
+        @SuppressLint("NewApi")
         public void exit() {
             // Note that at this point, we're leaving the tethered state.  We can fail any
             // of these operations, but it doesn't really change that we have to try them
@@ -1155,7 +1232,9 @@
                 try {
                     mNetd.tetherInterfaceRemove(mIfaceName);
                 } finally {
-                    mNetd.networkRemoveInterface(LOCAL_NET_ID, mIfaceName);
+                    if (mTetheringAgent == null) {
+                        mNetd.networkRemoveInterface(LOCAL_NET_ID, mIfaceName);
+                    }
                 }
             } catch (RemoteException | ServiceSpecificException e) {
                 mLastError = TETHER_ERROR_UNTETHER_IFACE_ERROR;
@@ -1165,6 +1244,11 @@
             stopIPv4();
             mBpfCoordinator.removeIpServer(IpServer.this);
 
+            if (mTetheringAgent != null && everRegistered(mTetheringAgent)) {
+                mTetheringAgent.unregister();
+                mTetheringAgent = null;
+            }
+
             resetLinkProperties();
 
             mTetheringMetrics.updateErrorCode(mInterfaceType, mLastError);
@@ -1184,6 +1268,12 @@
                     break;
                 case CMD_IPV6_TETHER_UPDATE:
                     updateUpstreamIPv6LinkProperties((LinkProperties) message.obj, message.arg1);
+                    // Sends update to the NetworkAgent.
+                    // TODO: Refactor the callers of sendLinkProperties()
+                    //  and move these code into sendLinkProperties().
+                    if (mTetheringAgent != null && everRegistered(mTetheringAgent)) {
+                        mTetheringAgent.sendLinkProperties(mLinkProperties);
+                    }
                     sendLinkProperties();
                     break;
                 case CMD_IP_FORWARDING_ENABLE_ERROR:
@@ -1236,13 +1326,17 @@
             // Remove deprecated routes from downstream network.
             final List<RouteInfo> routesToBeRemoved =
                     List.of(getDirectConnectedRoute(deprecatedLinkAddress));
-            removeRoutesFromNetwork(LOCAL_NET_ID, routesToBeRemoved);
+            if (mTetheringAgent == null) {
+                removeRoutesFromNetwork(LOCAL_NET_ID, routesToBeRemoved);
+            }
             for (RouteInfo route : routesToBeRemoved) mLinkProperties.removeRoute(route);
             mLinkProperties.removeLinkAddress(deprecatedLinkAddress);
 
             // Add new routes to downstream network.
             final List<RouteInfo> routesToBeAdded = List.of(getDirectConnectedRoute(mIpv4Address));
-            addRoutesToNetwork(LOCAL_NET_ID, routesToBeAdded);
+            if (mTetheringAgent == null) {
+                addRoutesToNetwork(LOCAL_NET_ID, routesToBeAdded);
+            }
             for (RouteInfo route : routesToBeAdded) mLinkProperties.addRoute(route);
             mLinkProperties.addLinkAddress(mIpv4Address);
 
@@ -1255,6 +1349,10 @@
                 mLog.e("Failed to update local DNS caching server");
                 return;
             }
+            // Sends update to the NetworkAgent.
+            if (mTetheringAgent != null && everRegistered(mTetheringAgent)) {
+                mTetheringAgent.sendLinkProperties(mLinkProperties);
+            }
             sendLinkProperties();
 
             // Notify DHCP server that new prefix/route has been applied on IpServer.
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 0cf008b..0730639 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -3105,7 +3105,7 @@
 
         mLog.i("adding IpServer for: " + iface);
         final TetherState tetherState = new TetherState(
-                new IpServer(iface, mHandler, interfaceType, mLog, mNetd, mBpfCoordinator,
+                new IpServer(iface, mContext, mHandler, interfaceType, mLog, mNetd, mBpfCoordinator,
                         mRoutingCoordinator, new ControlCallback(), mConfig, mTetheringMetrics,
                         mDeps.makeIpServerDependencies()), isNcm);
         mTetherStates.put(iface, tetherState);
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
index 5f0e5d0..e2609e7 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
@@ -148,6 +148,12 @@
      */
     public static final String TETHER_ACTIVE_SESSIONS_METRICS = "tether_active_sessions_metrics";
 
+    /**
+     * A feature flag to control whether the tethering local network agent should be enabled.
+     * Disabled by default.
+     */
+    public static final String TETHERING_LOCAL_NETWORK_AGENT = "tethering_local_network_agent";
+
     public final String[] tetherableUsbRegexs;
     public final String[] tetherableWifiRegexs;
     public final String[] tetherableWigigRegexs;
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/src/com/android/networkstack/tethering/util/TetheringUtils.java b/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java
index 79e6e16..9392ae8 100644
--- a/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java
+++ b/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java
@@ -15,8 +15,20 @@
  */
 package com.android.networkstack.tethering.util;
 
+import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
+import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.NetworkCapabilities.TRANSPORT_USB;
 import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
 import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
+import static android.net.TetheringManager.TETHERING_BLUETOOTH;
+import static android.net.TetheringManager.TETHERING_ETHERNET;
+import static android.net.TetheringManager.TETHERING_NCM;
+import static android.net.TetheringManager.TETHERING_USB;
+import static android.net.TetheringManager.TETHERING_VIRTUAL;
+import static android.net.TetheringManager.TETHERING_WIFI;
+import static android.net.TetheringManager.TETHERING_WIFI_P2P;
+import static android.net.TetheringManager.TETHERING_WIGIG;
 
 import android.net.TetherStatsParcel;
 import android.net.TetheringManager.TetheringRequest;
@@ -207,4 +219,30 @@
         request.getParcel().connectivityScope = CONNECTIVITY_SCOPE_GLOBAL;
         return request;
     }
+
+    /**
+     * Returns the transport type for the given interface type.
+     *
+     * @param interfaceType The interface type.
+     * @return The transport type.
+     * @throws IllegalArgumentException if the interface type is invalid.
+     */
+    public static int getTransportTypeForTetherableType(int interfaceType) {
+        switch (interfaceType) {
+            case TETHERING_WIFI:
+            case TETHERING_WIGIG:
+            case TETHERING_WIFI_P2P:
+                return TRANSPORT_WIFI;
+            case TETHERING_USB:
+            case TETHERING_NCM:
+                return TRANSPORT_USB;
+            case TETHERING_BLUETOOTH:
+                return TRANSPORT_BLUETOOTH;
+            case TETHERING_ETHERNET:
+            case TETHERING_VIRTUAL: // For virtual machines.
+                return TRANSPORT_ETHERNET;
+            default:
+                throw new IllegalArgumentException("Invalid interface type: " + interfaceType);
+        }
+    }
 }
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index 3a5728e..d47f4b3 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -17,7 +17,11 @@
 package android.net;
 
 import static android.Manifest.permission.DUMP;
+import static android.Manifest.permission.LOG_COMPAT_CHANGE;
+import static android.Manifest.permission.READ_COMPAT_CHANGE_CONFIG;
 import static android.net.InetAddresses.parseNumericAddress;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
+import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
 import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
 import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
 import static android.net.TetheringManager.TETHERING_ETHERNET;
@@ -27,6 +31,8 @@
 import static android.net.TetheringTester.buildUdpPacket;
 import static android.net.TetheringTester.isExpectedIcmpPacket;
 import static android.net.TetheringTester.isExpectedUdpDnsPacket;
+import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_MATCH_NON_THREAD_LOCAL_NETWORKS;
+import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
 import static android.system.OsConstants.ICMP_ECHO;
 import static android.system.OsConstants.ICMP_ECHOREPLY;
 import static android.system.OsConstants.IPPROTO_UDP;
@@ -49,6 +55,7 @@
 import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
 
+import android.app.compat.CompatChanges;
 import android.content.Context;
 import android.net.TetheringManager.TetheringRequest;
 import android.net.TetheringTester.TetheredDevice;
@@ -78,12 +85,16 @@
 import com.android.net.module.util.bpf.TetherStatsValue;
 import com.android.net.module.util.structs.Ipv4Header;
 import com.android.net.module.util.structs.UdpHeader;
+import com.android.testutils.AutoReleaseNetworkCallbackRule;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DeviceConfigRule;
 import com.android.testutils.DeviceInfoUtils;
 import com.android.testutils.DumpTestUtils;
 import com.android.testutils.NetworkStackModuleTest;
 import com.android.testutils.PollPacketReader;
+import com.android.testutils.RecorderCallback.CallbackEntry;
+import com.android.testutils.TestableNetworkCallback;
 
 import org.junit.After;
 import org.junit.Rule;
@@ -111,6 +122,12 @@
 public class EthernetTetheringTest extends EthernetTetheringTestBase {
     @Rule
     public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+    // For manipulating feature flag before and after testing.
+    @Rule
+    public final DeviceConfigRule mDeviceConfigRule = new DeviceConfigRule();
+    @Rule
+    public final AutoReleaseNetworkCallbackRule
+            mNetworkCallbackRule = new AutoReleaseNetworkCallbackRule();
 
     private static final String TAG = EthernetTetheringTest.class.getSimpleName();
 
@@ -200,6 +217,9 @@
             (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04  /* Address: 1.2.3.4 */
     };
 
+    // Shamelessly copied from TetheringConfiguration.
+    private static final String TETHERING_LOCAL_NETWORK_AGENT = "tethering_local_network_agent";
+
     @After
     public void tearDown() throws Exception {
         super.tearDown();
@@ -1224,4 +1244,49 @@
             maybeUnregisterTetheringEventCallback(tetheringEventCallback);
         }
     }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    public void testLocalAgent_networkCallbacks() throws Exception {
+        final boolean isMatchNonThreadLocalNetworksEnabled = runAsShell(
+                READ_COMPAT_CHANGE_CONFIG, LOG_COMPAT_CHANGE,
+                () -> CompatChanges.isChangeEnabled(ENABLE_MATCH_NON_THREAD_LOCAL_NETWORKS));
+        assumeTrue(isMatchNonThreadLocalNetworksEnabled);
+
+        mDeviceConfigRule.setConfig(NAMESPACE_TETHERING, TETHERING_LOCAL_NETWORK_AGENT, "1");
+        assumeFalse(isInterfaceForTetheringAvailable());
+        setIncludeTestInterfaces(true);
+
+        TestNetworkInterface downstreamIface = null;
+        MyTetheringEventCallback tetheringEventCallback = null;
+
+        final TestableNetworkCallback networkCallback = new TestableNetworkCallback();
+        final NetworkRequest networkRequest = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_LOCAL_NETWORK).build();
+        mNetworkCallbackRule.registerNetworkCallback(networkRequest, networkCallback);
+
+        try {
+            downstreamIface = createTestInterface();
+
+            final String iface = mTetheredInterfaceRequester.getInterface();
+            assertEquals("TetheredInterfaceCallback for unexpected interface",
+                    downstreamIface.getInterfaceName(), iface);
+
+            final TetheringRequest request = new TetheringRequest.Builder(TETHERING_ETHERNET)
+                    .setConnectivityScope(CONNECTIVITY_SCOPE_GLOBAL).build();
+            tetheringEventCallback = enableTethering(iface, request, null /* any upstream */);
+            tetheringEventCallback.awaitInterfaceTethered();
+
+            // Verify NetworkCallback works accordingly.
+            final Network network = networkCallback.expect(CallbackEntry.AVAILABLE).getNetwork();
+            final CallbackEntry.CapabilitiesChanged capEvent =
+                    networkCallback.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED);
+            assertEquals(network, capEvent.getNetwork());
+            assertTrue(capEvent.getCaps().hasTransport(TRANSPORT_ETHERNET));
+            assertTrue(capEvent.getCaps().hasCapability(NET_CAPABILITY_LOCAL_NETWORK));
+        } finally {
+            stopEthernetTethering(tetheringEventCallback);
+            maybeCloseTestInterface(downstreamIface);
+        }
+    }
 }
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index 84b301f..c935cbf35 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -28,6 +28,7 @@
 import static android.net.TetheringManager.TETHERING_WIFI_P2P;
 import static android.net.TetheringManager.TETHER_ERROR_ENABLE_FORWARDING_ERROR;
 import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_SERVICE_UNAVAIL;
 import static android.net.TetheringManager.TETHER_ERROR_TETHER_IFACE_ERROR;
 import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS;
 import static android.net.ip.IpServer.STATE_AVAILABLE;
@@ -37,7 +38,9 @@
 import static android.net.ip.IpServer.getTetherableIpv6Prefixes;
 
 import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
+import static com.android.modules.utils.build.SdkLevel.isAtLeastV;
 import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
+import static com.android.networkstack.tethering.TetheringConfiguration.TETHERING_LOCAL_NETWORK_AGENT;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -53,6 +56,7 @@
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
@@ -64,7 +68,7 @@
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
-import android.app.usage.NetworkStatsManager;
+import android.content.Context;
 import android.net.INetd;
 import android.net.InetAddresses;
 import android.net.InterfaceConfigurationParcel;
@@ -72,6 +76,8 @@
 import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.MacAddress;
+import android.net.Network;
+import android.net.NetworkAgent;
 import android.net.RouteInfo;
 import android.net.TetheringManager.TetheringRequest;
 import android.net.dhcp.DhcpServerCallbacks;
@@ -83,8 +89,10 @@
 import android.os.Build;
 import android.os.Handler;
 import android.os.RemoteException;
+import android.os.ServiceSpecificException;
 import android.os.test.TestLooper;
 import android.text.TextUtils;
+import android.util.ArrayMap;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -100,6 +108,7 @@
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.com.android.testutils.SetFeatureFlagsRule;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -122,6 +131,16 @@
     @Rule
     public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
 
+    final ArrayMap<String, Boolean> mFeatureFlags = new ArrayMap<>();
+    // 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));
+
     private static final String IFACE_NAME = "testnet1";
     private static final String UPSTREAM_IFACE = "upstream0";
     private static final String UPSTREAM_IFACE2 = "upstream1";
@@ -164,6 +183,7 @@
             new LinkAddress("2001:db8:0:abcd::168/64"));
     private static final Set<IpPrefix> UPSTREAM_PREFIXES2 = Set.of(
             new IpPrefix("2001:db8:0:1234::/64"), new IpPrefix("2001:db8:0:abcd::/64"));
+    private static final int TEST_NET_ID = 123;
 
     @Mock private INetd mNetd;
     @Mock private IpServer.Callback mCallback;
@@ -173,10 +193,11 @@
     @Mock private RouterAdvertisementDaemon mRaDaemon;
     @Mock private IpServer.Dependencies mDependencies;
     @Mock private RoutingCoordinatorManager mRoutingCoordinatorManager;
-    @Mock private NetworkStatsManager mStatsManager;
     @Mock private TetheringConfiguration mTetherConfig;
     @Mock private TetheringMetrics mTetheringMetrics;
     @Mock private BpfCoordinator mBpfCoordinator;
+    @Mock private Context mContext;
+    @Mock private NetworkAgent mNetworkAgent;
 
     @Captor private ArgumentCaptor<DhcpServingParamsParcel> mDhcpParamsCaptor;
 
@@ -205,6 +226,18 @@
         when(mDependencies.getInterfaceParams(UPSTREAM_IFACE)).thenReturn(UPSTREAM_IFACE_PARAMS);
         when(mDependencies.getInterfaceParams(UPSTREAM_IFACE2)).thenReturn(UPSTREAM_IFACE_PARAMS2);
         when(mDependencies.getInterfaceParams(IPSEC_IFACE)).thenReturn(IPSEC_IFACE_PARAMS);
+        doAnswer(
+                invocation -> mFeatureFlags.getOrDefault((String) invocation.getArgument(1), false)
+        ).when(mDependencies).isFeatureEnabled(any(), anyString());
+        if (isAtLeastV()) {
+            when(mDependencies.makeNetworkAgent(any(), any(), anyString(), anyInt(), any()))
+                    .thenReturn(mNetworkAgent);
+            // Mock the returned network and modifying the status.
+            final Network network = mock(Network.class);
+            doReturn(TEST_NET_ID).when(network).getNetId();
+            doReturn(network).when(mNetworkAgent).register();
+            doReturn(network).when(mNetworkAgent).getNetwork();
+        }
 
         mInterfaceConfiguration = new InterfaceConfigurationParcel();
         mInterfaceConfiguration.flags = new String[0];
@@ -294,10 +327,9 @@
     private IpServer createIpServer(final int interfaceType) {
         mLooper = new TestLooper();
         mHandler = new Handler(mLooper.getLooper());
-        return new IpServer(IFACE_NAME, mHandler, interfaceType, mSharedLog, mNetd, mBpfCoordinator,
-                mRoutingCoordinatorManager, mCallback, mTetherConfig,
+        return new IpServer(IFACE_NAME, mContext, mHandler, interfaceType, mSharedLog, mNetd,
+                mBpfCoordinator, mRoutingCoordinatorManager, mCallback, mTetherConfig,
                 mTetheringMetrics, mDependencies);
-
     }
 
     @Test
@@ -342,6 +374,10 @@
         verifyNoMoreInteractions(mNetd, mCallback);
     }
 
+    private boolean isTetheringNetworkAgentFeatureEnabled() {
+        return isAtLeastV() && mFeatureFlags.getOrDefault(TETHERING_LOCAL_NETWORK_AGENT, false);
+    }
+
     @Test
     public void canBeTetheredAsBluetooth() throws Exception {
         initStateMachine(TETHERING_BLUETOOTH);
@@ -360,10 +396,16 @@
                     IFACE_NAME.equals(cfg.ifName) && assertContainsFlag(cfg.flags, IF_STATE_UP)));
         }
         inOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME);
-        inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
-        // One for ipv4 route, one for ipv6 link local route.
-        inOrder.verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME),
-                any(), any());
+        if (isTetheringNetworkAgentFeatureEnabled()) {
+            inOrder.verify(mNetd, never()).networkAddInterface(anyInt(), anyString());
+            inOrder.verify(mNetd, never())
+                    .networkAddRoute(anyInt(), anyString(), anyString(), anyString());
+        } else {
+            inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
+            // One for ipv4 route, one for ipv6 link local route.
+            inOrder.verify(mNetd, times(2))
+                    .networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME), any(), any());
+        }
         inOrder.verify(mCallback).updateInterfaceState(
                 mIpServer, STATE_TETHERED, TETHER_ERROR_NO_ERROR);
         inOrder.verify(mCallback).updateLinkProperties(
@@ -379,7 +421,11 @@
         InOrder inOrder = inOrder(mCallback, mNetd, mRoutingCoordinatorManager);
         inOrder.verify(mNetd).tetherApplyDnsInterfaces();
         inOrder.verify(mNetd).tetherInterfaceRemove(IFACE_NAME);
-        inOrder.verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
+        if (isTetheringNetworkAgentFeatureEnabled()) {
+            inOrder.verify(mNetd, never()).networkRemoveInterface(anyInt(), anyString());
+        } else {
+            inOrder.verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
+        }
         // One is ipv4 address clear (set to 0.0.0.0), another is set interface down which only
         // happen after T. Before T, the interface configuration control in bluetooth side.
         if (isAtLeastT()) {
@@ -411,9 +457,15 @@
         inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg ->
                 IFACE_NAME.equals(cfg.ifName) && assertContainsFlag(cfg.flags, IF_STATE_UP)));
         inOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME);
-        inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
-        inOrder.verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME),
-                any(), any());
+        if (isTetheringNetworkAgentFeatureEnabled()) {
+            inOrder.verify(mNetd, never()).networkAddInterface(anyInt(), anyString());
+            inOrder.verify(mNetd, never())
+                    .networkAddRoute(anyInt(), anyString(), anyString(), anyString());
+        } else {
+            inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
+            inOrder.verify(mNetd, times(2))
+                    .networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME), any(), any());
+        }
         inOrder.verify(mCallback).updateInterfaceState(
                 mIpServer, STATE_TETHERED, TETHER_ERROR_NO_ERROR);
         inOrder.verify(mCallback).updateLinkProperties(
@@ -592,7 +644,11 @@
         inOrder.verify(mBpfCoordinator).clearAllIpv6Rules(mIpServer);
         inOrder.verify(mNetd).tetherApplyDnsInterfaces();
         inOrder.verify(mNetd).tetherInterfaceRemove(IFACE_NAME);
-        inOrder.verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
+        if (isTetheringNetworkAgentFeatureEnabled()) {
+            inOrder.verify(mNetd, never()).networkRemoveInterface(anyInt(), anyString());
+        } else {
+            inOrder.verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
+        }
         inOrder.verify(mNetd, times(isAtLeastT() ? 2 : 1)).interfaceSetCfg(
                 argThat(cfg -> IFACE_NAME.equals(cfg.ifName)));
         inOrder.verify(mRoutingCoordinatorManager).releaseDownstream(any());
@@ -1058,6 +1114,162 @@
         return true;
     }
 
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @SetFeatureFlagsRule.FeatureFlag(name = TETHERING_LOCAL_NETWORK_AGENT)
+    @Test
+    public void testTetheringNetworkAgent_tetheringAgentEnabled() throws Exception {
+        doTestTetheringNetworkAgent(CONNECTIVITY_SCOPE_GLOBAL, true);
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @SetFeatureFlagsRule.FeatureFlag(name = TETHERING_LOCAL_NETWORK_AGENT, enabled = false)
+    @Test
+    public void testTetheringNetworkAgent_tetheringAgentDisabled() throws Exception {
+        doTestTetheringNetworkAgent(CONNECTIVITY_SCOPE_GLOBAL, false);
+    }
+
+    // Verify Tethering Network Agent feature doesn't affect Wi-fi P2P Group Owner although
+    // the code is mostly shared.
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @SetFeatureFlagsRule.FeatureFlag(name = TETHERING_LOCAL_NETWORK_AGENT)
+    @Test
+    public void testTetheringNetworkAgent_p2pGroupOwnerAgentDisabled() throws Exception {
+        doTestTetheringNetworkAgent(CONNECTIVITY_SCOPE_LOCAL, false);
+    }
+
+    private void doTestTetheringNetworkAgent(int scope, boolean expectAgentEnabled)
+            throws Exception {
+        initStateMachine(TETHERING_USB);
+
+        final InOrder inOrder = inOrder(mNetworkAgent, mNetd);
+        dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED,
+                0, createMockTetheringRequest(scope));
+
+        inOrder.verify(mNetworkAgent, expectAgentEnabled ? times(1) : never()).register();
+        inOrder.verify(mNetd, times(1)).tetherInterfaceAdd(anyString());
+        if (expectAgentEnabled) {
+            inOrder.verify(mNetd, never()).networkAddInterface(anyInt(), anyString());
+            inOrder.verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), any(), any());
+            inOrder.verify(mNetworkAgent, times(1)).sendLinkProperties(any());
+            inOrder.verify(mNetworkAgent, times(1)).markConnected();
+        } else {
+            inOrder.verify(mNetd, times(1)).networkAddInterface(anyInt(), anyString());
+            inOrder.verify(mNetd, times(2)).networkAddRoute(anyInt(), anyString(), any(), any());
+            inOrder.verify(mNetworkAgent, never()).sendLinkProperties(any());
+            inOrder.verify(mNetworkAgent, never()).markConnected();
+        }
+
+        dispatchCommand(IpServer.CMD_TETHER_UNREQUESTED);
+        if (expectAgentEnabled) {
+            inOrder.verify(mNetworkAgent, times(1)).unregister();
+            inOrder.verify(mNetd, never()).networkRemoveInterface(anyInt(), anyString());
+        } else {
+            inOrder.verify(mNetworkAgent, never()).unregister();
+            inOrder.verify(mNetd, times(1)).networkRemoveInterface(anyInt(), anyString());
+        }
+    }
+
+    // Verify if the registration failed, tethering can be gracefully shutdown.
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @SetFeatureFlagsRule.FeatureFlag(name = TETHERING_LOCAL_NETWORK_AGENT)
+    @Test
+    public void testTetheringNetworkAgent_registerThrows() throws Exception {
+        initStateMachine(TETHERING_USB);
+
+        final InOrder inOrder = inOrder(mNetworkAgent, mNetd, mCallback);
+        doReturn(null).when(mNetworkAgent).getNetwork();
+        doThrow(IllegalStateException.class).when(mNetworkAgent).register();
+        dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED,
+                0, createMockTetheringRequest(CONNECTIVITY_SCOPE_GLOBAL));
+
+        inOrder.verify(mNetworkAgent).register();
+        inOrder.verify(mNetd, never()).networkCreate(any());
+        inOrder.verify(mNetworkAgent, never()).sendLinkProperties(any());
+        inOrder.verify(mNetworkAgent, never()).markConnected();
+        inOrder.verify(mNetworkAgent, never()).unregister();
+        inOrder.verify(mNetd, never()).networkDestroy(anyInt());
+        inOrder.verify(mCallback).updateInterfaceState(
+                mIpServer, STATE_AVAILABLE, TETHER_ERROR_SERVICE_UNAVAIL);
+    }
+
+    // Verify if the network creation failed, tethering can be gracefully shutdown.
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @SetFeatureFlagsRule.FeatureFlag(name = TETHERING_LOCAL_NETWORK_AGENT)
+    @Test
+    public void testTetheringNetworkAgent_netdThrows() throws Exception {
+        initStateMachine(TETHERING_USB);
+
+        final InOrder inOrder = inOrder(mNetworkAgent, mNetd, mCallback);
+        doThrow(ServiceSpecificException.class).when(mNetd).tetherInterfaceAdd(any());
+        dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED,
+                0, createMockTetheringRequest(CONNECTIVITY_SCOPE_GLOBAL));
+
+        inOrder.verify(mNetworkAgent).register();
+        inOrder.verify(mNetd, never()).networkCreate(any());
+        inOrder.verify(mNetworkAgent, never()).sendLinkProperties(any());
+        inOrder.verify(mNetworkAgent, never()).markConnected();
+        inOrder.verify(mNetworkAgent).unregister();
+        inOrder.verify(mNetd, never()).networkDestroy(anyInt());
+        inOrder.verify(mCallback).updateInterfaceState(
+                mIpServer, STATE_AVAILABLE, TETHER_ERROR_TETHER_IFACE_ERROR);
+    }
+
+    // Verify when IPv6 address update, set routes accordingly.
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @SetFeatureFlagsRule.FeatureFlag(name = TETHERING_LOCAL_NETWORK_AGENT)
+    @Test
+    public void testTetheringNetworkAgent_ipv6AddressUpdate() throws Exception {
+        initStateMachine(TETHERING_USB);
+
+        final InOrder inOrder = inOrder(mNetworkAgent, mNetd);
+        dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED,
+                0, createMockTetheringRequest(CONNECTIVITY_SCOPE_GLOBAL));
+
+        inOrder.verify(mNetworkAgent).register();
+        inOrder.verify(mNetd, never()).networkCreate(any());
+        inOrder.verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), any(), any());
+
+        // Ipv6 link local route won't show up in the LinkProperties, so just
+        // verify ipv4 route.
+        final ArgumentCaptor<LinkProperties> lpCaptor =
+                ArgumentCaptor.forClass(LinkProperties.class);
+        inOrder.verify(mNetworkAgent).sendLinkProperties(lpCaptor.capture());
+        final RouteInfo expectedIpv4Route = new RouteInfo(PrefixUtils.asIpPrefix(mTestAddress),
+                null, IFACE_NAME, RouteInfo.RTN_UNICAST);
+        assertRoutes(List.of(expectedIpv4Route), lpCaptor.getValue().getRoutes());
+        assertEquals(IFACE_NAME, lpCaptor.getValue().getInterfaceName());
+
+        inOrder.verify(mNetworkAgent).markConnected();
+
+        // Mock ipv4-only upstream show up.
+        dispatchTetherConnectionChanged(UPSTREAM_IFACE);
+        inOrder.verifyNoMoreInteractions();
+
+        // Verify LinkProperties is updated when IPv6 connectivity is available.
+        final LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName(UPSTREAM_IFACE);
+        lp.setLinkAddresses(UPSTREAM_ADDRESSES);
+        dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, -1);
+        inOrder.verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), any(), any());
+        inOrder.verify(mNetworkAgent).sendLinkProperties(lpCaptor.capture());
+
+        // Expect one Ipv4 route, plus one Ipv6 route.
+        final RouteInfo expectedIpv6Route = new RouteInfo(UPSTREAM_PREFIXES.toArray(
+                new IpPrefix[0])[0], null, IFACE_NAME, RouteInfo.RTN_UNICAST);
+        assertRoutes(List.of(expectedIpv4Route, expectedIpv6Route),
+                lpCaptor.getValue().getRoutes());
+        assertEquals(IFACE_NAME, lpCaptor.getValue().getInterfaceName());
+
+        dispatchCommand(IpServer.CMD_TETHER_UNREQUESTED);
+        inOrder.verify(mNetworkAgent).unregister();
+        inOrder.verify(mNetd, never()).networkDestroy(anyInt());
+    }
+
+    private void assertRoutes(List<RouteInfo> expectedRoutes, List<RouteInfo> actualRoutes) {
+        assertTrue("Expected Routes: " + expectedRoutes + ", but got: " + actualRoutes,
+                expectedRoutes.equals(actualRoutes));
+    }
+
     @Test @IgnoreUpTo(Build.VERSION_CODES.R)
     public void dadProxyUpdates() throws Exception {
         InOrder inOrder = inOrder(mDadProxy);
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/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index dc3cbd2..d659815 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -75,6 +75,7 @@
 
 import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
 import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
+import static com.android.modules.utils.build.SdkLevel.isAtLeastV;
 import static com.android.net.module.util.Inet4AddressUtils.inet4AddressToIntHTH;
 import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
 import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH;
@@ -83,6 +84,7 @@
 import static com.android.networkstack.tethering.TestConnectivityManager.BROADCAST_FIRST;
 import static com.android.networkstack.tethering.TestConnectivityManager.CALLBACKS_FIRST;
 import static com.android.networkstack.tethering.Tethering.UserRestrictionActionListener;
+import static com.android.networkstack.tethering.TetheringConfiguration.TETHERING_LOCAL_NETWORK_AGENT;
 import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_FORCE_USB_FUNCTIONS;
 import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_USB_NCM_FUNCTION;
 import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_USB_RNDIS_FUNCTION;
@@ -192,6 +194,7 @@
 import android.telephony.PhoneStateListener;
 import android.telephony.TelephonyManager;
 import android.test.mock.MockContentResolver;
+import android.util.ArrayMap;
 import android.util.ArraySet;
 
 import androidx.annotation.NonNull;
@@ -219,6 +222,7 @@
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.MiscAsserts;
+import com.android.testutils.com.android.testutils.SetFeatureFlagsRule;
 
 import org.junit.After;
 import org.junit.Before;
@@ -250,6 +254,16 @@
 public class TetheringTest {
     @Rule public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
 
+    final ArrayMap<String, Boolean> mFeatureFlags = new ArrayMap<>();
+    // 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));
+
     private static final int IFINDEX_OFFSET = 100;
 
     private static final String TEST_MOBILE_IFNAME = "test_rmnet_data0";
@@ -461,6 +475,11 @@
         public void setOnDhcpServerCreatedResult(final int result) {
             mOnDhcpServerCreatedResult = result;
         }
+
+        @Override
+        public boolean isFeatureEnabled(Context context, String name) {
+            return mFeatureFlags.getOrDefault(name, false);
+        }
     }
 
     public class MockTetheringDependencies extends TetheringDependencies {
@@ -954,12 +973,18 @@
         verifyNoMoreInteractions(mCm);
     }
 
-    private void verifyInterfaceServingModeStarted(String ifname) throws Exception {
+    private void verifyInterfaceServingModeStarted(String ifname, boolean expectAgentEnabled)
+            throws Exception {
         verify(mNetd).interfaceSetCfg(any(InterfaceConfigurationParcel.class));
         verify(mNetd).tetherInterfaceAdd(ifname);
-        verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, ifname);
-        verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(ifname),
-                anyString(), anyString());
+        if (expectAgentEnabled) {
+            verify(mNetd, never()).networkAddInterface(anyInt(), anyString());
+            verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), anyString(), anyString());
+        } else {
+            verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, ifname);
+            verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(ifname),
+                    anyString(), anyString());
+        }
     }
 
     private void verifyTetheringBroadcast(String ifname, String whichExtra) {
@@ -1061,10 +1086,18 @@
         failingLocalOnlyHotspotLegacyApBroadcast(false);
     }
 
-    private void verifyStopHotpot() throws Exception {
+    private boolean isTetheringNetworkAgentFeatureEnabled() {
+        return isAtLeastV() && mFeatureFlags.getOrDefault(TETHERING_LOCAL_NETWORK_AGENT, false);
+    }
+
+    private void verifyStopHotpot(boolean isLocalOnly) throws Exception {
         verify(mNetd).tetherApplyDnsInterfaces();
         verify(mNetd).tetherInterfaceRemove(TEST_WLAN_IFNAME);
-        verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, TEST_WLAN_IFNAME);
+        if (!isLocalOnly && isTetheringNetworkAgentFeatureEnabled()) {
+            verify(mNetd, never()).networkRemoveInterface(anyInt(), anyString());
+        } else {
+            verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, TEST_WLAN_IFNAME);
+        }
         // interfaceSetCfg() called once for enabling and twice disabling IPv4.
         verify(mNetd, times(3)).interfaceSetCfg(any(InterfaceConfigurationParcel.class));
         verify(mNetd).tetherStop();
@@ -1083,7 +1116,8 @@
     }
 
     private void verifyStartHotspot(boolean isLocalOnly) throws Exception {
-        verifyInterfaceServingModeStarted(TEST_WLAN_IFNAME);
+        final boolean expectAgentEnabled = !isLocalOnly && isTetheringNetworkAgentFeatureEnabled();
+        verifyInterfaceServingModeStarted(TEST_WLAN_IFNAME, expectAgentEnabled);
         verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
         verify(mWifiManager).updateInterfaceIpState(
                 TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
@@ -1127,7 +1161,7 @@
         mTethering.interfaceRemoved(TEST_WLAN_IFNAME);
         mLooper.dispatchAll();
 
-        verifyStopHotpot();
+        verifyStopHotpot(true /* isLocalOnly */);
     }
 
     /**
@@ -2073,7 +2107,7 @@
         mTethering.interfaceRemoved(TEST_WLAN_IFNAME);
         mLooper.dispatchAll();
 
-        verifyStopHotpot();
+        verifyStopHotpot(false /* isLocalOnly */);
     }
 
     @Test
@@ -2218,9 +2252,14 @@
         // code is refactored the two calls during shutdown will revert to one.
         verify(mNetd, times(3)).interfaceSetCfg(argThat(p -> TEST_WLAN_IFNAME.equals(p.ifName)));
         verify(mNetd, times(1)).tetherInterfaceAdd(TEST_WLAN_IFNAME);
-        verify(mNetd, times(1)).networkAddInterface(INetd.LOCAL_NET_ID, TEST_WLAN_IFNAME);
-        verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(TEST_WLAN_IFNAME),
-                anyString(), anyString());
+        if (isTetheringNetworkAgentFeatureEnabled()) {
+            verify(mNetd, never()).networkAddInterface(anyInt(), anyString());
+            verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), anyString(), anyString());
+        } else {
+            verify(mNetd, times(1)).networkAddInterface(INetd.LOCAL_NET_ID, TEST_WLAN_IFNAME);
+            verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(TEST_WLAN_IFNAME),
+                    anyString(), anyString());
+        }
         verify(mWifiManager).updateInterfaceIpState(
                 TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
         verify(mWifiManager).updateInterfaceIpState(
@@ -2239,7 +2278,11 @@
         // so it can take down AP mode.
         verify(mNetd, times(1)).tetherApplyDnsInterfaces();
         verify(mNetd, times(1)).tetherInterfaceRemove(TEST_WLAN_IFNAME);
-        verify(mNetd, times(1)).networkRemoveInterface(INetd.LOCAL_NET_ID, TEST_WLAN_IFNAME);
+        if (isTetheringNetworkAgentFeatureEnabled()) {
+            verify(mNetd, never()).networkRemoveInterface(anyInt(), anyString());
+        } else {
+            verify(mNetd, times(1)).networkRemoveInterface(INetd.LOCAL_NET_ID, TEST_WLAN_IFNAME);
+        }
         verify(mWifiManager).updateInterfaceIpState(
                 TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_CONFIGURATION_ERROR);
 
@@ -3100,7 +3143,7 @@
         }
         sendWifiP2pConnectionChanged(true, true, TEST_P2P_IFNAME);
 
-        verifyInterfaceServingModeStarted(TEST_P2P_IFNAME);
+        verifyInterfaceServingModeStarted(TEST_P2P_IFNAME, false);
         verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_AVAILABLE_TETHER);
         verify(mNetd, times(1)).ipfwdEnableForwarding(TETHERING_NAME);
         verify(mNetd, times(1)).tetherStartWithConfiguration(any());
@@ -4119,13 +4162,22 @@
                     && assertContainsFlag(cfg.flags, INetd.IF_STATE_UP)));
         }
         verify(mNetd).tetherInterfaceAdd(TEST_BT_IFNAME);
-        verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, TEST_BT_IFNAME);
-        verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(TEST_BT_IFNAME),
-                anyString(), anyString());
+        if (isTetheringNetworkAgentFeatureEnabled()) {
+            verify(mNetd, never()).networkAddInterface(anyInt(), anyString());
+            verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), anyString(), anyString());
+        } else {
+            verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, TEST_BT_IFNAME);
+            verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(TEST_BT_IFNAME),
+                    anyString(), anyString());
+        }
         verify(mNetd).ipfwdEnableForwarding(TETHERING_NAME);
         verify(mNetd).tetherStartWithConfiguration(any());
-        verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(TEST_BT_IFNAME),
-                anyString(), anyString());
+        if (isTetheringNetworkAgentFeatureEnabled()) {
+            verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), anyString(), anyString());
+        } else {
+            verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(TEST_BT_IFNAME),
+                    anyString(), anyString());
+        }
         verifyNoMoreInteractions(mNetd);
         reset(mNetd);
     }
@@ -4140,7 +4192,11 @@
     private void verifyNetdCommandForBtTearDown() throws Exception {
         verify(mNetd).tetherApplyDnsInterfaces();
         verify(mNetd).tetherInterfaceRemove(TEST_BT_IFNAME);
-        verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, TEST_BT_IFNAME);
+        if (isTetheringNetworkAgentFeatureEnabled()) {
+            verify(mNetd, never()).networkRemoveInterface(anyInt(), anyString());
+        } else {
+            verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, TEST_BT_IFNAME);
+        }
         // One is ipv4 address clear (set to 0.0.0.0), another is set interface down which only
         // happen after T. Before T, the interface configuration control in bluetooth side.
         verify(mNetd, times(isAtLeastT() ? 2 : 1)).interfaceSetCfg(
@@ -4405,7 +4461,7 @@
         initTetheringOnTestThread();
         // Enable wifi P2P.
         sendWifiP2pConnectionChanged(true, true, TEST_P2P_IFNAME);
-        verifyInterfaceServingModeStarted(TEST_P2P_IFNAME);
+        verifyInterfaceServingModeStarted(TEST_P2P_IFNAME, false);
         verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_AVAILABLE_TETHER);
         verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_ACTIVE_LOCAL_ONLY);
         verify(mUpstreamNetworkMonitor).startObserveUpstreamNetworks();
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 aca544a..bdc2e8c 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;
             }
         }
@@ -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/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index 8355d31..1d9c235 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -360,8 +360,7 @@
         mUnderlyingNetworks = null;
         mEnterpriseId = 0;
         mReservationId = RES_ID_UNSET;
-        // TODO: Change to default disabled when introduce this filtering.
-        mMatchNonThreadLocalNetworks = true;
+        mMatchNonThreadLocalNetworks = false;
     }
 
     /**
@@ -2961,8 +2960,7 @@
      * 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;
+    private boolean mMatchNonThreadLocalNetworks;
 
     /**
      * Returns the match non-thread local networks flag.
diff --git a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
index 2261c69..acf8e85 100644
--- a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
+++ b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
@@ -139,14 +139,25 @@
 
     /**
      * 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 = 36)
+    public static final long RESTRICT_LOCAL_NETWORK = 365139289L;
+
+    /**
+     * Enable match non-threads local networks.
+     *
+     * Apps targeting a release after V can have NetworkRequests matches non-thread local networks.
+     *
      * @hide
      */
     @ChangeId
-    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT)
-    public static final long RESTRICT_LOCAL_NETWORK = 365139289L;
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public static final long ENABLE_MATCH_NON_THREAD_LOCAL_NETWORKS = 349487600L;
 
     private ConnectivityCompatChanges() {
     }
diff --git a/framework/src/android/net/connectivity/ConnectivityInternalApiUtil.java b/framework/src/android/net/connectivity/ConnectivityInternalApiUtil.java
index ba39ca0..9478e91 100644
--- a/framework/src/android/net/connectivity/ConnectivityInternalApiUtil.java
+++ b/framework/src/android/net/connectivity/ConnectivityInternalApiUtil.java
@@ -16,11 +16,25 @@
 
 package android.net.connectivity;
 
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED;
+
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.net.ConnectivityManager;
+import android.net.LinkProperties;
+import android.net.LocalNetworkConfig;
+import android.net.NetworkAgent;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkScore;
 import android.os.Build;
 import android.os.IBinder;
+import android.os.Looper;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 
 /**
@@ -57,4 +71,36 @@
         final ConnectivityManager cm = ctx.getSystemService(ConnectivityManager.class);
         return cm.getRoutingCoordinatorService();
     }
+
+    /**
+     * Create a NetworkAgent instance to be used by Tethering.
+     * @param ctx the context
+     * @return an instance of the {@code NetworkAgent}
+     */
+    // TODO: Expose LocalNetworkConfig related APIs and delete this method. This method is
+    //  only here because on R Tethering is installed and not Connectivity, requiring all
+    //  shared classes to be public API. LocalNetworkConfig is not public yet, but it will
+    //  only be used by Tethering on V+ so it's fine.
+    @SuppressLint("WrongConstant")
+    @NonNull
+    public static NetworkAgent buildTetheringNetworkAgent(@NonNull Context ctx,
+            @NonNull Looper looper, @NonNull String logTag, int transportType,
+            @NonNull LinkProperties lp) {
+        final NetworkCapabilities.Builder builder = new NetworkCapabilities.Builder()
+                .addCapability(NET_CAPABILITY_NOT_METERED)
+                .addCapability(NET_CAPABILITY_NOT_ROAMING)
+                .addCapability(NET_CAPABILITY_NOT_CONGESTED)
+                .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                .addTransportType(transportType);
+        // TODO: Change to use the constant definition. Flags.netCapabilityLocalNetwork() was not
+        //  fully rolled out but the service will still process this capability, set it anyway.
+        builder.addCapability(36 /* NET_CAPABILITY_LOCAL_NETWORK */);
+        final NetworkCapabilities caps = builder.build();
+        final NetworkAgentConfig nac = new NetworkAgentConfig.Builder().build();
+        return new NetworkAgent(ctx, looper, logTag, caps, lp,
+                new LocalNetworkConfig.Builder().build(), new NetworkScore.Builder()
+                .setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK)
+                .build(), nac, null /* provider */) {
+        };
+    }
 }
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 48467ed..def701b 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -18,6 +18,10 @@
 
 import static android.net.EthernetManager.ETHERNET_STATE_DISABLED;
 import static android.net.EthernetManager.ETHERNET_STATE_ENABLED;
+import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_LOWPAN;
+import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
 import static android.net.TestNetworkManager.TEST_TAP_PREFIX;
 
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
@@ -65,6 +69,7 @@
 import java.net.NetworkInterface;
 import java.net.SocketException;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Enumeration;
 import java.util.Iterator;
 import java.util.List;
@@ -639,7 +644,7 @@
             nc = mNetworkCapabilities.get(hwAddress);
             if (nc == null) {
                 final boolean isTestIface = iface.matches(TEST_IFACE_REGEXP);
-                nc = createDefaultNetworkCapabilities(isTestIface, /* overrideTransport */ null);
+                nc = createDefaultNetworkCapabilities(isTestIface, TRANSPORT_ETHERNET);
             }
         }
 
@@ -753,7 +758,7 @@
      * <interface name|mac address>;[Network Capabilities];[IP config];[Override Transport]}
      */
     private void parseEthernetConfig(String configString) {
-        final EthernetTrackerConfig config = createEthernetTrackerConfig(configString);
+        final EthernetTrackerConfig config = new EthernetTrackerConfig(configString);
         NetworkCapabilities nc;
         // Starting with Android B (API level 36), we provide default NetworkCapabilities
         // for Ethernet interfaces when no explicit capabilities are specified in the
@@ -765,11 +770,11 @@
         // 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)) {
+        if (SdkLevel.isAtLeastB() && config.mCaps.isEmpty()) {
             boolean isTestIface = config.mIface.matches(TEST_IFACE_REGEXP);
             nc = createDefaultNetworkCapabilities(isTestIface, config.mTransport);
         } else {
-            nc = createNetworkCapabilities(config.mCapabilities, config.mTransport).build();
+            nc = createNetworkCapabilities(config.mCaps, config.mTransport).build();
         }
         mNetworkCapabilities.put(config.mIface, nc);
 
@@ -779,16 +784,10 @@
         }
     }
 
-    @VisibleForTesting
-    static EthernetTrackerConfig createEthernetTrackerConfig(@NonNull final String configString) {
-        Objects.requireNonNull(configString, "EthernetTrackerConfig requires non-null config");
-        return new EthernetTrackerConfig(configString.split(";", /* limit of tokens */ 4));
-    }
-
     private static NetworkCapabilities createDefaultNetworkCapabilities(
-            boolean isTestIface, @Nullable String overrideTransport) {
+            boolean isTestIface, int transportType) {
         NetworkCapabilities.Builder builder =
-                createNetworkCapabilities(/* commaSeparatedCapabilities */ null, overrideTransport)
+                createNetworkCapabilities(Collections.emptyList(), transportType)
                         .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
                         .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
                         .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
@@ -808,19 +807,18 @@
     /**
      * Parses a static list of network capabilities
      *
-     * @param commaSeparatedCapabilities A comma separated string list of integer encoded
-     *                                   NetworkCapability.NET_CAPABILITY_* values
+     * @param capabilities      A List of NetworkCapabilities.
      * @param overrideTransport A string representing a single integer encoded override transport
      *                          type. Must be one of the NetworkCapability.TRANSPORT_*
      *                          values. TRANSPORT_VPN is not supported. Errors with input
      *                          will cause the override to be ignored.
      */
     @VisibleForTesting
-    static NetworkCapabilities.Builder createNetworkCapabilities(
-            @Nullable String commaSeparatedCapabilities, @Nullable String overrideTransport) {
+    static NetworkCapabilities.Builder createNetworkCapabilities(List<Integer> capabilities,
+            int transportType) {
 
         final NetworkCapabilities.Builder builder =
-                TextUtils.isEmpty(commaSeparatedCapabilities)
+                capabilities.isEmpty()
                         ? new NetworkCapabilities.Builder()
                         : NetworkCapabilities.Builder.withoutDefaultCapabilities();
 
@@ -828,50 +826,13 @@
         // attempt to add it. Since we can only have one override, all errors with it will
         // gracefully default back to TRANSPORT_ETHERNET and warn the user. VPN is not allowed as an
         // override type. Wifi Aware and LoWPAN are currently unsupported as well.
-        int transport = NetworkCapabilities.TRANSPORT_ETHERNET;
-        if (!TextUtils.isEmpty(overrideTransport)) {
-            try {
-                int parsedTransport = Integer.valueOf(overrideTransport);
-                if (parsedTransport == NetworkCapabilities.TRANSPORT_VPN
-                        || parsedTransport == NetworkCapabilities.TRANSPORT_WIFI_AWARE
-                        || parsedTransport == NetworkCapabilities.TRANSPORT_LOWPAN) {
-                    Log.e(TAG, "Override transport '" + parsedTransport + "' is not supported. "
-                            + "Defaulting to TRANSPORT_ETHERNET");
-                } else {
-                    transport = parsedTransport;
-                }
-            } catch (NumberFormatException nfe) {
-                Log.e(TAG, "Override transport type '" + overrideTransport + "' "
-                        + "could not be parsed. Defaulting to TRANSPORT_ETHERNET");
-            }
-        }
-
-        // Apply the transport. If the user supplied a valid number that is not a valid transport
-        // then adding will throw an exception. Default back to TRANSPORT_ETHERNET if that happens
-        try {
-            builder.addTransportType(transport);
-        } catch (IllegalArgumentException iae) {
-            Log.e(TAG, transport + " is not a valid NetworkCapability.TRANSPORT_* value. "
-                    + "Defaulting to TRANSPORT_ETHERNET");
-            builder.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET);
-        }
+        builder.addTransportType(transportType);
 
         builder.setLinkUpstreamBandwidthKbps(100 * 1000);
         builder.setLinkDownstreamBandwidthKbps(100 * 1000);
 
-        if (!TextUtils.isEmpty(commaSeparatedCapabilities)) {
-            for (String strNetworkCapability : commaSeparatedCapabilities.split(",")) {
-                if (!TextUtils.isEmpty(strNetworkCapability)) {
-                    try {
-                        builder.addCapability(Integer.valueOf(strNetworkCapability));
-                    } catch (NumberFormatException nfe) {
-                        Log.e(TAG, "Capability '" + strNetworkCapability + "' could not be parsed");
-                    } catch (IllegalArgumentException iae) {
-                        Log.e(TAG, strNetworkCapability + " is not a valid "
-                                + "NetworkCapability.NET_CAPABILITY_* value");
-                    }
-                }
-            }
+        for (int capability : capabilities) {
+            builder.addCapability(capability);
         }
         // Ethernet networks have no way to update the following capabilities, so they always
         // have them.
@@ -1053,16 +1014,67 @@
     @VisibleForTesting
     static class EthernetTrackerConfig {
         final String mIface;
-        final String mCapabilities;
+        final List<Integer> mCaps;
         final String mIpConfig;
-        final String mTransport;
+        final int mTransport;
 
-        EthernetTrackerConfig(@NonNull final String[] tokens) {
-            Objects.requireNonNull(tokens, "EthernetTrackerConfig requires non-null tokens");
+        private static List<Integer> parseCapabilities(String capabilitiesString) {
+            if (TextUtils.isEmpty(capabilitiesString)) {
+                return Collections.emptyList();
+            }
+
+            final ArrayList<Integer> capabilities = new ArrayList<>();
+            for (String strNetworkCapability : capabilitiesString.split(",")) {
+                if (TextUtils.isEmpty(strNetworkCapability)) {
+                    continue;
+                }
+                final Integer capability;
+                try {
+                    capability = Integer.valueOf(strNetworkCapability);
+                } catch (NumberFormatException e) {
+                    Log.e(TAG, "Failed to parse capability: " + strNetworkCapability, e);
+                    continue;
+                }
+                capabilities.add(capability);
+            }
+            return capabilities;
+        }
+
+        private static int parseTransportType(String transportString) {
+            if (TextUtils.isEmpty(transportString)) {
+                return TRANSPORT_ETHERNET;
+            }
+
+            final int parsedTransport;
+            try {
+                parsedTransport = Integer.valueOf(transportString);
+            } catch (NumberFormatException e) {
+                Log.e(TAG, "Failed to parse transport type", e);
+                return TRANSPORT_ETHERNET;
+            }
+
+            if (!NetworkCapabilities.isValidTransport(parsedTransport)) {
+                return TRANSPORT_ETHERNET;
+            }
+
+            switch (parsedTransport) {
+                case TRANSPORT_VPN:
+                case TRANSPORT_WIFI_AWARE:
+                case TRANSPORT_LOWPAN:
+                    Log.e(TAG, "Unsupported transport type '" + parsedTransport + "'");
+                    return TRANSPORT_ETHERNET;
+                default:
+                    return parsedTransport;
+            }
+        }
+
+        EthernetTrackerConfig(String configString) {
+            Objects.requireNonNull(configString, "EthernetTrackerConfig requires non-null config");
+            final String[] tokens = configString.split(";", /* limit of tokens */ 4);
             mIface = tokens[0];
-            mCapabilities = tokens.length > 1 ? tokens[1] : null;
+            mCaps = tokens.length > 1 ? parseCapabilities(tokens[1]) : Collections.emptyList();
             mIpConfig = tokens.length > 2 && !TextUtils.isEmpty(tokens[2]) ? tokens[2] : null;
-            mTransport = tokens.length > 3 ? tokens[3] : null;
+            mTransport = tokens.length > 3 ? parseTransportType(tokens[3]) : TRANSPORT_ETHERNET;
         }
     }
 }
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/config_thread.xml b/service/ServiceConnectivityResources/res/values/config_thread.xml
index a458c7f..62b12fb 100644
--- a/service/ServiceConnectivityResources/res/values/config_thread.xml
+++ b/service/ServiceConnectivityResources/res/values/config_thread.xml
@@ -46,11 +46,12 @@
     <bool name="config_thread_location_use_for_country_code_enabled">true</bool>
 
     <!-- Specifies the UTF-8 vendor name of this device. If this value is not an empty string, it
-    will be included in TXT value (key is 'vn') of the "_meshcop._udp" mDNS service which is
-    published by the Thread service. A non-empty string value must not exceed length of 24 UTF-8
-    bytes.
+    will be included in TXT value (key is 'vn') of the "_meshcop._udp" mDNS service as well as the
+    Vendor Name TLV for network diagnostic. A non-empty string value must not exceed length of 24
+    UTF-8 bytes. A special value "ro.product.manufacturer" indicates this value should be derived
+    from the `ro.product.manufacturer` system property.
     -->
-    <string translatable="false" name="config_thread_vendor_name">Android</string>
+    <string translatable="false" name="config_thread_vendor_name">ro.product.manufacturer</string>
 
     <!-- Specifies the 24 bits vendor OUI of this device. If this value is not an empty string, it
     will be included in TXT (key is 'vo') value of the "_meshcop._udp" mDNS service which is
@@ -61,11 +62,12 @@
     <string translatable="false" name="config_thread_vendor_oui"></string>
 
     <!-- Specifies the UTF-8 product model name of this device. If this value is not an empty
-    string, it will be included in TXT (key is 'mn') value of the "_meshcop._udp" mDNS service
-    which is published by the Thread service. A non-empty string value must not exceed length of 24
-    UTF-8 bytes.
+    string, it will be included in TXT (key is 'mn') value of the "_meshcop._udp" mDNS service as
+    well as the Vendor Model TLV for network diagnostic. A non-empty string value must not exceed
+    length of 24 UTF-8 bytes. A special value "ro.product.model" indicates this value should be
+    derived from the `ro.product.model` system property.
     -->
-    <string translatable="false" name="config_thread_model_name">Thread Border Router</string>
+    <string translatable="false" name="config_thread_model_name">ro.product.model</string>
 
     <!-- Specifies vendor-specific mDNS TXT entries which will be included in the "_meshcop._udp"
     service. The TXT entries list MUST conform to the format requirement in RFC 6763 section 6. For
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..2c44b62 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;
@@ -120,6 +121,7 @@
 import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_TEST;
 import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_TEST_ONLY;
 import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_MATCH_LOCAL_NETWORK;
+import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_MATCH_NON_THREAD_LOCAL_NETWORKS;
 import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_SELF_CERTIFIED_CAPABILITIES_DECLARATION;
 import static android.net.connectivity.ConnectivityCompatChanges.NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION;
 import static android.os.Process.INVALID_UID;
@@ -429,6 +431,7 @@
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
+import java.util.function.Predicate;
 
 /**
  * @hide
@@ -1471,6 +1474,10 @@
             return SdkLevel.isAtLeastV();
         }
 
+        public boolean isAtLeastB() {
+            return SdkLevel.isAtLeastB();
+        }
+
         /**
          * Get system properties to use in ConnectivityService.
          */
@@ -1931,8 +1938,8 @@
         mUseDeclaredMethodsForCallbacksEnabled =
                 mDeps.isFeatureNotChickenedOut(context,
                         ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS);
-        mQueueNetworkAgentEventsInSystemServer =
-                mDeps.isFeatureNotChickenedOut(context,
+        mQueueNetworkAgentEventsInSystemServer = mDeps.isAtLeastB()
+                && mDeps.isFeatureNotChickenedOut(context,
                         ConnectivityFlags.QUEUE_NETWORK_AGENT_EVENTS_IN_SYSTEM_SERVER);
         // registerUidFrozenStateChangedCallback is only available on U+
         mQueueCallbacksForFrozenApps = mDeps.isAtLeastU()
@@ -3184,6 +3191,11 @@
     }
 
     private void maybeDisableLocalNetworkMatching(NetworkCapabilities nc, int callingUid) {
+        // If disabled, NetworkRequest cannot match non-thread local networks even if
+        // specified explicitly. Compat change is enabled by default on apps targeting B+.
+        // Agent should not be visible on U- even if it's rolled out.
+        nc.setMatchNonThreadLocalNetworks(mDeps.isAtLeastV() && mDeps.isChangeEnabled(
+                ENABLE_MATCH_NON_THREAD_LOCAL_NETWORKS, callingUid));
         if (mDeps.isChangeEnabled(ENABLE_MATCH_LOCAL_NETWORK, callingUid)) {
             return;
         }
@@ -4893,7 +4905,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 +5339,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 +5459,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 +5527,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 +5675,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 +5692,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 +5865,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 +6165,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 +6234,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 +6612,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 +6957,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 +7035,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 +7429,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 +7512,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 +7564,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 +9132,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 +10503,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 +10515,7 @@
                         mCarrierPrivilegeAuthenticator);
                 updateCapabilities(nai.getScore(), nai, nc);
             }
-        }
+        });
     }
 
     /**
@@ -11208,7 +11272,11 @@
                 break;
             }
         }
-        nai.disconnect();
+        if (mQueueNetworkAgentEventsInSystemServer) {
+            disconnectAndDestroyNetwork(nai);
+        } else {
+            nai.disconnect();
+        }
     }
 
     private void handleLingerComplete(NetworkAgentInfo oldNetwork) {
@@ -11368,7 +11436,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 +11448,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 +11670,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 +11776,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 +11811,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 +11863,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 +11888,7 @@
                     teardownUnneededNetwork(nai);
                 }
             }
-        }
+        });
     }
 
     /**
@@ -12215,7 +12277,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 +12403,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 +12411,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 +12420,7 @@
                             newBlockedState);
                 }
             }
-        }
+        });
     }
 
     @VisibleForTesting
@@ -12447,11 +12509,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 +13404,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 +13578,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 +14368,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 +14379,7 @@
                             mIngressRateLimit);
                 }
             }
-        }
+        });
     }
 
     private boolean canNetworkBeRateLimited(@NonNull final NetworkAgentInfo networkAgent) {
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/devicetests/com/android/testutils/TestableNetworkAgent.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
index 0413ed4..f5f2e69 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
@@ -17,6 +17,7 @@
 package com.android.testutils
 
 import android.content.Context
+import android.net.ConnectivityManager
 import android.net.ConnectivityManager.FEATURE_QUEUE_NETWORK_AGENT_EVENTS_IN_SYSTEM_SERVER
 import android.net.InetAddresses.parseNumericAddress
 import android.net.KeepalivePacketData
@@ -77,9 +78,7 @@
 private class Provider(context: Context, looper: Looper) :
             NetworkProvider(context, looper, "NetworkAgentTest NetworkProvider")
 
-private val enabledFeatures = mutableMapOf(
-    FEATURE_QUEUE_NETWORK_AGENT_EVENTS_IN_SYSTEM_SERVER to true
-)
+private val enabledFeatures = mutableMapOf<Long, Boolean>()
 
 public open class TestableNetworkAgent(
     context: Context,
@@ -92,7 +91,12 @@
 
     override fun isFeatureEnabled(context: Context, feature: Long): Boolean {
         when (val it = enabledFeatures.get(feature)) {
-            null -> fail("Unmocked feature $feature, see TestableNetworkAgent.enabledFeatures")
+            null -> {
+                val cm = context.getSystemService(ConnectivityManager::class.java)
+                val res = cm.isFeatureEnabled(feature)
+                enabledFeatures[feature] = res
+                return res
+            }
             else -> return it
         }
     }
diff --git a/tests/cts/hostside/app/AndroidManifest.xml b/tests/cts/hostside/app/AndroidManifest.xml
index e0f4cdc..2ef53b9 100644
--- a/tests/cts/hostside/app/AndroidManifest.xml
+++ b/tests/cts/hostside/app/AndroidManifest.xml
@@ -27,6 +27,7 @@
     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
     <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
+    <uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
     <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
     <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
index 6c92b74..29e2ebb 100755
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
@@ -267,6 +267,11 @@
         mNetwork = null;
         mTestContext = getInstrumentation().getContext();
         mTargetContext = getInstrumentation().getTargetContext();
+        getInstrumentation()
+                .getUiAutomation()
+                .grantRuntimePermission(
+                        "com.android.cts.net.hostside",
+                        "android.permission.NEARBY_WIFI_DEVICES");
         storePrivateDnsSetting();
         mDevice = UiDevice.getInstance(getInstrumentation());
         mActivity = launchActivity(mTargetContext.getPackageName(), MyActivity.class);
diff --git a/tests/cts/multidevices/apfv4_test.py b/tests/cts/multidevices/apfv4_test.py
index aa535fd..0fc1093 100644
--- a/tests/cts/multidevices/apfv4_test.py
+++ b/tests/cts/multidevices/apfv4_test.py
@@ -15,10 +15,12 @@
 from absl.testing import parameterized
 from mobly import asserts
 from net_tests_utils.host.python import apf_test_base, apf_utils
+from scapy.layers.l2 import Ether
 
 # Constants.
 COUNTER_DROPPED_ETHERTYPE_NOT_ALLOWED = "DROPPED_ETHERTYPE_NOT_ALLOWED"
 ETHER_BROADCAST_ADDR = "FFFFFFFFFFFF"
+MIN_PACKET_SIZE = 60
 
 
 class ApfV4Test(apf_test_base.ApfTestBase, parameterized.TestCase):
@@ -45,20 +47,18 @@
   # Tests can use any disallowed packet type. Currently,
   # several ethertypes from the legacy ApfFilter denylist are used.
   @parameterized.parameters(
-      "88a2",  # ATA over Ethernet
-      "88a4",  # EtherCAT
-      "88b8",  # GOOSE (Generic Object Oriented Substation event)
-      "88cd",  # SERCOS III
-      "88e3",  # Media Redundancy Protocol (IEC62439-2)
+      0x88a2,  # ATA over Ethernet
+      0x88a4,  # EtherCAT
+      0x88b8,  # GOOSE (Generic Object Oriented Substation event)
+      0x88cd,  # SERCOS III
+      0x88e3,  # Media Redundancy Protocol (IEC62439-2)
   )  # Declare inputs for state_str and expected_result.
   def test_apf_drop_ethertype_not_allowed(self, blocked_ether_type):
-    # Ethernet header (14 bytes).
-    packet = self.client_mac_address.replace(":", "")  # Destination MAC
-    packet += self.server_mac_address.replace(":", "")  # Source MAC
-    packet += blocked_ether_type
+    eth = Ether(src=self.server_mac_address, dst=self.client_mac_address, type=blocked_ether_type)
+    packet = bytes(eth).hex()
 
-    # Pad with zeroes to minimum ethernet frame length.
-    packet += "00" * 46
+    # Add zero padding up to minimum ethernet frame length
+    packet = packet.ljust(MIN_PACKET_SIZE * 2, "0")
     self.send_packet_and_expect_counter_increased(
         packet, COUNTER_DROPPED_ETHERTYPE_NOT_ALLOWED
     )
diff --git a/tests/cts/multidevices/apfv6_test.py b/tests/cts/multidevices/apfv6_test.py
index 2404966..9b2e309 100644
--- a/tests/cts/multidevices/apfv6_test.py
+++ b/tests/cts/multidevices/apfv6_test.py
@@ -190,15 +190,18 @@
 
         igmp = IGMPv3mr(records=mcast_records)
         expected_igmpv3_report = bytes(ether/ip/igmpv3_hdr/igmp).hex()
-        self.send_packet_and_expect_reply_received(
-            igmpv3_general_query, "DROPPED_IGMP_V3_GENERAL_QUERY_REPLIED", expected_igmpv3_report
-        )
-
-        for addr in mcast_addrs:
-            adb_utils.adb_shell(
-                self.clientDevice,
-                f'ip addr del {addr}/32 dev {self.client_iface_name}'
+        try:
+            self.send_packet_and_expect_reply_received(
+                igmpv3_general_query,
+                "DROPPED_IGMP_V3_GENERAL_QUERY_REPLIED",
+                expected_igmpv3_report
             )
+        finally:
+            for addr in mcast_addrs:
+                adb_utils.adb_shell(
+                    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)
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..02a5d1f 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
+            )
         }
     }
 
@@ -1005,6 +1044,12 @@
             mock(Network::class.java),
             mock(INetworkAgentRegistry::class.java)
         )
+        doReturn(SHOULD_CREATE_NETWORKS_IMMEDIATELY).`when`(mockCm)
+            .isFeatureEnabled(
+                eq(ConnectivityManager.FEATURE_QUEUE_NETWORK_AGENT_EVENTS_IN_SYSTEM_SERVER)
+            )
+        doReturn(Context.CONNECTIVITY_SERVICE).`when`(mockContext)
+            .getSystemServiceName(ConnectivityManager::class.java)
         doReturn(mockCm).`when`(mockContext).getSystemService(Context.CONNECTIVITY_SERVICE)
         doReturn(mockedResult).`when`(mockCm).registerNetworkAgent(
             any(),
@@ -1660,16 +1705,25 @@
 
         // 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.expect<Lost>(network1, 2_000 /* timeoutMs */)
+        if (SHOULD_CREATE_NETWORKS_IMMEDIATELY) {
+            // This is the correct sequence of events.
+            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)
+        } else {
+            // This is incorrect and fixed by the "create networks immediately" feature
+            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.expect<Lost>(network1, 2_000 /* timeoutMs */)
+        }
         agent1.expectCallback<OnNetworkUnwanted>()
 
         // Test lingering:
@@ -1717,7 +1771,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 +1841,19 @@
 
         val (newWifiAgent, newWifiNetwork) = connectNetwork(TRANSPORT_WIFI)
         testCallback.expectAvailableCallbacks(newWifiNetwork, validated = true)
-        matchAllCallback.expectAvailableThenValidatedCallbacks(newWifiNetwork)
-        matchAllCallback.expect<Lost>(wifiNetwork)
+        if (SHOULD_CREATE_NETWORKS_IMMEDIATELY) {
+            // This is the correct sequence of events
+            matchAllCallback.expectAvailableCallbacks(newWifiNetwork, validated = false)
+            matchAllCallback.expect<Lost>(wifiNetwork)
+            matchAllCallback.expectCaps(newWifiNetwork) {
+                it.hasCapability(NET_CAPABILITY_VALIDATED)
+            }
+        } else {
+            // When networks are not created immediately, the sequence is slightly incorrect
+            // and instead is as follows
+            matchAllCallback.expectAvailableThenValidatedCallbacks(newWifiNetwork)
+            matchAllCallback.expect<Lost>(wifiNetwork)
+        }
         wifiAgent.expectCallback<OnNetworkUnwanted>()
         testCallback.expect<CapabilitiesChanged>(newWifiNetwork)
 
@@ -1848,8 +1913,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..8c90e01 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -586,6 +586,10 @@
             "https://android.com/user/api/capport/";
     private static final String TEST_FRIENDLY_NAME = "Network friendly name";
     private static final String TEST_REDIRECT_URL = "http://example.com/firstPath";
+    private static final String QUEUE_NETWORK_AGENT_EVENTS_IN_SYSTEM_SERVER =
+            "queue_network_agent_events_in_system_server";
+
+    private boolean mShouldCreateNetworksImmediately;
 
     private MockContext mServiceContext;
     private HandlerThread mCsHandlerThread;
@@ -1935,6 +1939,9 @@
         mService.mLingerDelayMs = TEST_LINGER_DELAY_MS;
         mService.mNascentDelayMs = TEST_NASCENT_DELAY_MS;
 
+        mShouldCreateNetworksImmediately = mService.isConnectivityServiceFeatureEnabledForTesting(
+                QUEUE_NETWORK_AGENT_EVENTS_IN_SYSTEM_SERVER);
+
         if (mDeps.isAtLeastV()) {
             verify(mNetworkPolicyManager, never()).registerNetworkPolicyCallback(any(), any());
             mPolicyCallback = null;
@@ -2234,7 +2241,8 @@
         private static final int VERSION_T = 3;
         private static final int VERSION_U = 4;
         private static final int VERSION_V = 5;
-        private static final int VERSION_MAX = VERSION_V;
+        private static final int VERSION_B = 6;
+        private static final int VERSION_MAX = VERSION_B;
         private int mSdkLevel = VERSION_UNMOCKED;
 
         private void setBuildSdk(final int sdkLevel) {
@@ -2264,6 +2272,12 @@
         }
 
         @Override
+        public boolean isAtLeastB() {
+            return mSdkLevel == VERSION_UNMOCKED ? super.isAtLeastB()
+                    : mSdkLevel >= VERSION_B;
+        }
+
+        @Override
         public BpfNetMaps getBpfNetMaps(Context context, INetd netd,
                 InterfaceTracker interfaceTracker) {
             return mBpfNetMaps;
@@ -3093,22 +3107,43 @@
         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) {
-            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);
+        if (mShouldCreateNetworksImmediately) {
+            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);
+            }
         } else {
-            net1.expectDisconnected(TEST_CALLBACK_TIMEOUT_MS);
+            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) {
+                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);
+            } else {
+                net1.expectDisconnected(TEST_CALLBACK_TIMEOUT_MS);
+            }
+            net1.disconnect();
+            generalCb.expect(LOST, net1);
         }
-        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/CSKeepConnectedTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt
index 4aeae19..c0965b4 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt
@@ -19,7 +19,7 @@
 import android.net.LocalNetworkConfig
 import android.net.NetworkCapabilities
 import android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK
-import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkCapabilities.TRANSPORT_THREAD
 import android.net.NetworkRequest
 import android.net.NetworkScore
 import android.net.NetworkScore.KEEP_CONNECTED_FOR_TEST
@@ -42,7 +42,7 @@
     fun testKeepConnectedLocalAgent() {
         deps.setBuildSdk(VERSION_V)
         val nc = NetworkCapabilities.Builder()
-                .addTransportType(TRANSPORT_WIFI)
+                .addTransportType(TRANSPORT_THREAD)
                 .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
                 .build()
         val keepConnectedAgent = Agent(
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
index 6805d9a..6dc9d2d 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
@@ -18,14 +18,11 @@
 
 import android.content.pm.PackageManager.FEATURE_LEANBACK
 import android.net.INetd
-import android.net.LocalNetworkConfig
 import android.net.NativeNetworkConfig
 import android.net.NativeNetworkType
 import android.net.NetworkCapabilities
 import android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK
 import android.net.NetworkRequest
-import android.net.NetworkScore
-import android.net.NetworkScore.KEEP_CONNECTED_FOR_TEST
 import android.net.VpnManager
 import android.os.Build
 import androidx.test.filters.SmallTest
@@ -46,11 +43,6 @@
 private const val TIMEOUT_MS = 2_000L
 private const val NO_CALLBACK_TIMEOUT_MS = 200L
 
-private fun keepConnectedScore() =
-        FromS(NetworkScore.Builder().setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST).build())
-
-private fun defaultLnc() = FromS(LocalNetworkConfig.Builder().build())
-
 @DevSdkIgnoreRunner.MonitorThreadLeak
 @RunWith(DevSdkIgnoreRunner::class)
 @SmallTest
@@ -103,7 +95,7 @@
         }
         cm.registerNetworkCallback(request.build(), allNetworksCb)
         val ncTemplate = NetworkCapabilities.Builder().run {
-            addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+            addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
             addCapability(NET_CAPABILITY_LOCAL_NETWORK)
         }.build()
         val localAgent = if (params.sdkLevel >= VERSION_V ||
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
index d835155..9468d54 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
@@ -39,6 +39,7 @@
 import android.net.NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK
 import android.net.RouteInfo
 import android.net.connectivity.ConnectivityCompatChanges.ENABLE_MATCH_LOCAL_NETWORK
+import android.net.connectivity.ConnectivityCompatChanges.ENABLE_MATCH_NON_THREAD_LOCAL_NETWORKS
 import android.os.Build
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
@@ -129,6 +130,7 @@
     @Test
     fun testStructuralConstraintViolation() {
         deps.setBuildSdk(VERSION_V)
+        deps.setChangeIdEnabled(true, ENABLE_MATCH_NON_THREAD_LOCAL_NETWORKS)
 
         val cb = TestableNetworkCallback()
         cm.requestNetwork(
@@ -162,6 +164,7 @@
     @Test
     fun testUpdateLocalAgentConfig() {
         deps.setBuildSdk(VERSION_V)
+        deps.setChangeIdEnabled(true, ENABLE_MATCH_NON_THREAD_LOCAL_NETWORKS)
 
         val cb = TestableNetworkCallback()
         cm.requestNetwork(
@@ -428,6 +431,7 @@
     @Test
     fun testMulticastRoutingConfig_UpstreamSelectorCellToWifi() {
         deps.setBuildSdk(VERSION_V)
+        deps.setChangeIdEnabled(true, ENABLE_MATCH_NON_THREAD_LOCAL_NETWORKS)
         val cb = TestableNetworkCallback()
         cm.registerNetworkCallback(
                 NetworkRequest.Builder().clearCapabilities()
@@ -500,6 +504,7 @@
     @Test
     fun testMulticastRoutingConfig_UpstreamSelectorWifiToNull() {
         deps.setBuildSdk(VERSION_V)
+        deps.setChangeIdEnabled(true, ENABLE_MATCH_NON_THREAD_LOCAL_NETWORKS)
         val cb = TestableNetworkCallback()
         cm.registerNetworkCallback(
                 NetworkRequest.Builder().clearCapabilities()
@@ -577,6 +582,7 @@
 
     fun doTestUnregisterUpstreamAfterReplacement(sameIfaceName: Boolean) {
         deps.setBuildSdk(VERSION_V)
+        deps.setChangeIdEnabled(true, ENABLE_MATCH_NON_THREAD_LOCAL_NETWORKS)
         val cb = TestableNetworkCallback()
         cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities().build(), cb)
 
@@ -657,6 +663,7 @@
     @Test
     fun testUnregisterUpstreamAfterReplacement_neverReplaced() {
         deps.setBuildSdk(VERSION_V)
+        deps.setChangeIdEnabled(true, ENABLE_MATCH_NON_THREAD_LOCAL_NETWORKS)
         val cb = TestableNetworkCallback()
         cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities().build(), cb)
 
@@ -710,6 +717,7 @@
     @Test
     fun testUnregisterLocalAgentAfterReplacement() {
         deps.setBuildSdk(VERSION_V)
+        deps.setChangeIdEnabled(true, ENABLE_MATCH_NON_THREAD_LOCAL_NETWORKS)
 
         val localCb = TestableNetworkCallback()
         cm.requestNetwork(
@@ -775,6 +783,7 @@
     @Test
     fun testDestroyedNetworkAsSelectedUpstream() {
         deps.setBuildSdk(VERSION_V)
+        deps.setChangeIdEnabled(true, ENABLE_MATCH_NON_THREAD_LOCAL_NETWORKS)
         val cb = TestableNetworkCallback()
         cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities().build(), cb)
 
@@ -827,6 +836,7 @@
     @Test
     fun testForwardingRules() {
         deps.setBuildSdk(VERSION_V)
+        deps.setChangeIdEnabled(true, ENABLE_MATCH_NON_THREAD_LOCAL_NETWORKS)
         // Set up a local agent that should forward its traffic to the best DUN upstream.
         val lnc = FromS(
                 LocalNetworkConfig.Builder()
@@ -965,6 +975,7 @@
 
     fun doTestLocalNetworkUnwanted(haveUpstream: Boolean) {
         deps.setBuildSdk(VERSION_V)
+        deps.setChangeIdEnabled(true, ENABLE_MATCH_NON_THREAD_LOCAL_NETWORKS)
 
         val nr = NetworkRequest.Builder().addCapability(NET_CAPABILITY_LOCAL_NETWORK).build()
         val requestCb = TestableNetworkCallback()
@@ -1075,4 +1086,86 @@
                 expectCallback = true
         )
     }
+
+    @Test
+    fun testNonThreadLocalAgentMatches_disabled() {
+        doTestNonThreadLocalAgentMatches(
+                VERSION_V,
+                enableMatchNonThreadLocalNetworks = false,
+                TRANSPORT_WIFI,
+                expectAvailable = false
+        )
+        doTestNonThreadLocalAgentMatches(
+                VERSION_V,
+                enableMatchNonThreadLocalNetworks = false,
+                TRANSPORT_THREAD,
+                expectAvailable = true
+        )
+        doTestNonThreadLocalAgentMatches(
+                VERSION_B,
+                enableMatchNonThreadLocalNetworks = false,
+                TRANSPORT_WIFI,
+                expectAvailable = false
+        )
+        doTestNonThreadLocalAgentMatches(
+                VERSION_B,
+                enableMatchNonThreadLocalNetworks = false,
+                TRANSPORT_THREAD,
+                expectAvailable = true
+        )
+    }
+
+    @Test
+    fun testNonThreadLocalAgentMatches_enabled() {
+        doTestNonThreadLocalAgentMatches(
+                VERSION_V,
+                enableMatchNonThreadLocalNetworks = true,
+                TRANSPORT_WIFI,
+                expectAvailable = true
+        )
+        doTestNonThreadLocalAgentMatches(
+                VERSION_V,
+                enableMatchNonThreadLocalNetworks = true,
+                TRANSPORT_THREAD,
+                expectAvailable = true
+        )
+        doTestNonThreadLocalAgentMatches(
+                VERSION_B,
+                enableMatchNonThreadLocalNetworks = true,
+                TRANSPORT_WIFI,
+                expectAvailable = true
+        )
+        doTestNonThreadLocalAgentMatches(
+                VERSION_B,
+                enableMatchNonThreadLocalNetworks = true,
+                TRANSPORT_THREAD,
+                expectAvailable = true
+        )
+    }
+
+    private fun doTestNonThreadLocalAgentMatches(
+            sdkLevel: Int,
+            enableMatchNonThreadLocalNetworks: Boolean,
+            transport: Int,
+            expectAvailable: Boolean
+    ) {
+        deps.setBuildSdk(sdkLevel)
+        deps.setChangeIdEnabled(
+                enableMatchNonThreadLocalNetworks,
+                ENABLE_MATCH_NON_THREAD_LOCAL_NETWORKS
+        )
+
+        val localNcTemplate = NetworkCapabilities.Builder().run {
+            addTransportType(transport)
+            addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+        }.build()
+        val localAgent = Agent(
+                nc = localNcTemplate,
+                score = keepConnectedScore(),
+                lnc = defaultLnc()
+        )
+        // The implementation inside connect() will expect the OnAvailable.
+        localAgent.connect(expectAvailable)
+        localAgent.disconnect(expectAvailable)
+    }
 }
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt
index 35f8ae5..e6a69c3 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt
@@ -20,6 +20,7 @@
 import android.net.NetworkRequest
 import android.os.Build
 import android.os.Process
+import android.text.TextUtils
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
 import com.android.testutils.TestableNetworkCallback
@@ -54,9 +55,17 @@
         cm.requestNetwork(CELL_INTERNET_NOT_METERED_NR, TestableNetworkCallback())
         waitForIdle()
 
+        // Some fields might be altered by the service, e.g. mMatchNonThreadLocalNetworks.
+        // Check immutable differences instead.
         verify(networkRequestStateStatsMetrics).onNetworkRequestReceived(
-                argThat{req -> req.networkCapabilities.equals(
-                        CELL_INTERNET_NOT_METERED_NR.networkCapabilities)})
+                argThat { req ->
+                    TextUtils.isEmpty(
+                            req.networkCapabilities.describeImmutableDifferences(
+                                    CELL_INTERNET_NOT_METERED_NR.networkCapabilities
+                            )
+                    )
+                }
+        )
     }
 
     @Test
@@ -77,7 +86,13 @@
         cm.unregisterNetworkCallback(cb)
         waitForIdle()
         verify(networkRequestStateStatsMetrics).onNetworkRequestRemoved(
-                argThat{req -> req.networkCapabilities.equals(
-                        CELL_INTERNET_NOT_METERED_NR.networkCapabilities)})
+                argThat { req ->
+                    TextUtils.isEmpty(
+                            req.networkCapabilities.describeImmutableDifferences(
+                                    CELL_INTERNET_NOT_METERED_NR.networkCapabilities
+                            )
+                    )
+                }
+        )
     }
 }
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSQueuedCallbacksTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSQueuedCallbacksTest.kt
index fc2a06c..6d1858e 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSQueuedCallbacksTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSQueuedCallbacksTest.kt
@@ -39,6 +39,7 @@
 import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
 import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
 import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_THREAD
 import android.net.NetworkCapabilities.TRANSPORT_WIFI
 import android.net.NetworkPolicyManager.NetworkPolicyCallback
 import android.net.NetworkRequest
@@ -356,6 +357,7 @@
         val localAgent = Agent(
             nc = defaultNc()
                 .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                .addTransportType(TRANSPORT_THREAD)
                 .removeCapability(NET_CAPABILITY_INTERNET),
             lp = defaultLp().apply { interfaceName = "local42" },
             lnc = FromS(lnc)
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
index 6e07ac6..a77daa8 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
@@ -137,7 +137,7 @@
         nmCallbacks.notifyNetworkTestedWithExtras(p)
     }
 
-    fun connect() {
+    fun connect(expectAvailable: Boolean = true) {
         val mgr = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
         val request = NetworkRequest.Builder().apply {
             clearCapabilities()
@@ -149,26 +149,30 @@
         val cb = TestableNetworkCallback()
         mgr.registerNetworkCallback(request, cb)
         agent.markConnected()
-        if (null == cb.poll { it is Available && agent.network == it.network }) {
-            if (!nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED) &&
-                    nc.hasTransport(TRANSPORT_CELLULAR)) {
-                // ConnectivityService adds NOT_SUSPENDED by default to all non-cell agents. An
-                // agent without NOT_SUSPENDED will not connect, instead going into the SUSPENDED
-                // state, so this call will not terminate.
-                // Instead of forcefully adding NOT_SUSPENDED to all agents like older tools did,
-                // it's better to let the developer manage it as they see fit but help them
-                // debug if they forget.
-                fail(
-                    "Could not connect the agent. Did you forget to add " +
-                        "NET_CAPABILITY_NOT_SUSPENDED ?"
-                )
+        if (expectAvailable) {
+            if (null == cb.poll { it is Available && agent.network == it.network }) {
+                if (!nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED) &&
+                        nc.hasTransport(TRANSPORT_CELLULAR)) {
+                    // ConnectivityService adds NOT_SUSPENDED by default to all non-cell agents. An
+                    // agent without NOT_SUSPENDED will not connect, instead going into the
+                    // SUSPENDED state, so this call will not terminate.
+                    // Instead of forcefully adding NOT_SUSPENDED to all agents like older tools did,
+                    // it's better to let the developer manage it as they see fit but help them
+                    // debug if they forget.
+                    fail(
+                        "Could not connect the agent. Did you forget to add " +
+                            "NET_CAPABILITY_NOT_SUSPENDED ?"
+                    )
+                }
+                fail("Could not connect the agent. Instrumentation failure ?")
             }
-            fail("Could not connect the agent. Instrumentation failure ?")
+        } else {
+            cb.assertNoCallback()
         }
         mgr.unregisterNetworkCallback(cb)
     }
 
-    fun disconnect() {
+    fun disconnect(expectAvailable: Boolean = true) {
         val mgr = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
         val request = NetworkRequest.Builder().apply {
             clearCapabilities()
@@ -179,9 +183,14 @@
         }.build()
         val cb = TestableNetworkCallback(timeoutMs = SHORT_TIMEOUT_MS)
         mgr.registerNetworkCallback(request, cb)
-        cb.eventuallyExpect<Available> { it.network == agent.network }
-        agent.unregister()
-        cb.eventuallyExpect<Lost> { it.network == agent.network }
+        if (expectAvailable) {
+            cb.eventuallyExpect<Available> { it.network == agent.network }
+            agent.unregister()
+            cb.eventuallyExpect<Lost> { it.network == agent.network }
+        } else {
+            agent.unregister()
+            cb.assertNoCallback()
+        }
     }
 
     fun setTeardownDelayMillis(delayMillis: Int) = agent.setTeardownDelayMillis(delayMillis)
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
index 48333c5..0fe61ec 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -112,7 +112,8 @@
 internal const val VERSION_T = 3
 internal const val VERSION_U = 4
 internal const val VERSION_V = 5
-internal const val VERSION_MAX = VERSION_V
+internal const val VERSION_B = 6
+internal const val VERSION_MAX = VERSION_B
 
 internal const val CALLING_UID_UNMOCKED = Process.INVALID_UID
 
@@ -388,6 +389,7 @@
         override fun isAtLeastT() = if (isSdkUnmocked) super.isAtLeastT() else sdkLevel >= VERSION_T
         override fun isAtLeastU() = if (isSdkUnmocked) super.isAtLeastU() else sdkLevel >= VERSION_U
         override fun isAtLeastV() = if (isSdkUnmocked) super.isAtLeastV() else sdkLevel >= VERSION_V
+        override fun isAtLeastB() = if (isSdkUnmocked) super.isAtLeastB() else sdkLevel >= VERSION_B
 
         private var callingUid = CALLING_UID_UNMOCKED
 
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
index 5e18843..f28f063 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
@@ -34,6 +34,7 @@
 import android.net.IpPrefix
 import android.net.LinkAddress
 import android.net.LinkProperties
+import android.net.LocalNetworkConfig
 import android.net.NetworkAgentConfig
 import android.net.NetworkCapabilities
 import android.net.NetworkScore
@@ -84,6 +85,11 @@
 
 internal fun defaultScore() = FromS(NetworkScore.Builder().build())
 
+internal fun keepConnectedScore() = FromS(NetworkScore.Builder()
+                .setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_FOR_TEST).build())
+
+internal fun defaultLnc() = FromS(LocalNetworkConfig.Builder().build())
+
 internal fun defaultLp() = LinkProperties().apply {
     addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
     addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
index 533bbf8..6ce7dbc 100644
--- a/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
+++ b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
@@ -18,6 +18,8 @@
 
 import static android.net.TestNetworkManager.TEST_TAP_PREFIX;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertThrows;
@@ -44,6 +46,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.server.ethernet.EthernetTracker.EthernetTrackerConfig;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.HandlerUtils;
@@ -283,34 +286,57 @@
 
     private void assertParsedNetworkCapabilities(
             NetworkCapabilities expectedNetworkCapabilities,
-            String configCapabiltiies,
+            String configCapabilities,
             String configTransports) {
+        final String ipConfig = "";
+        final String configString =
+                String.join(";", TEST_IFACE, configCapabilities, ipConfig, configTransports);
+        final EthernetTrackerConfig config = new EthernetTrackerConfig(configString);
         assertEquals(
                 expectedNetworkCapabilities,
-                EthernetTracker.createNetworkCapabilities(configCapabiltiies, configTransports)
-                        .build());
+                EthernetTracker.createNetworkCapabilities(config.mCaps, config.mTransport).build());
     }
 
     @Test
     public void testCreateEthernetTrackerConfigReturnsCorrectValue() {
-        final String capabilities = "2";
+        final String capabilities = "2,4,6,8";
         final String ipConfig = "3";
-        final String transport = "4";
+        final String transport = "1";
         final String configString = String.join(";", TEST_IFACE, capabilities, ipConfig, transport);
 
-        final EthernetTracker.EthernetTrackerConfig config =
-                EthernetTracker.createEthernetTrackerConfig(configString);
+        final EthernetTrackerConfig config = new EthernetTrackerConfig(configString);
 
         assertEquals(TEST_IFACE, config.mIface);
-        assertEquals(capabilities, config.mCapabilities);
+        assertThat(config.mCaps).containsExactly(2, 4, 6, 8);
         assertEquals(ipConfig, config.mIpConfig);
-        assertEquals(transport, config.mTransport);
+        assertEquals(NetworkCapabilities.TRANSPORT_WIFI, config.mTransport);
+    }
+
+    @Test
+    public void testCreateEthernetTrackerConfig_withInvalidTransport() {
+        final String capabilities = "2";
+        final String ipConfig = "3";
+        final String transport = "100"; // Invalid transport type
+        final String configString = String.join(";", TEST_IFACE, capabilities, ipConfig, transport);
+
+        final EthernetTrackerConfig config = new EthernetTrackerConfig(configString);
+        assertEquals(NetworkCapabilities.TRANSPORT_ETHERNET, config.mTransport);
+    }
+
+    @Test
+    public void testCreateEthernetTrackerConfig_withDisallowedTransport() {
+        final String capabilities = "2";
+        final String ipConfig = "3";
+        final String transport = "4"; // TRANSPORT_VPN is not allowed
+        final String configString = String.join(";", TEST_IFACE, capabilities, ipConfig, transport);
+
+        final EthernetTrackerConfig config = new EthernetTrackerConfig(configString);
+        assertEquals(NetworkCapabilities.TRANSPORT_ETHERNET, config.mTransport);
     }
 
     @Test
     public void testCreateEthernetTrackerConfigThrowsNpeWithNullInput() {
-        assertThrows(NullPointerException.class,
-                () -> EthernetTracker.createEthernetTrackerConfig(null));
+        assertThrows(NullPointerException.class, () -> new EthernetTrackerConfig(null));
     }
 
     @Test
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/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index d859fb2..e99c88e 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -128,6 +128,7 @@
 import com.android.net.module.util.SharedLog;
 import com.android.server.ServiceManagerWrapper;
 import com.android.server.connectivity.ConnectivityResources;
+import com.android.server.connectivity.MockableSystemProperties;
 import com.android.server.thread.openthread.BackboneRouterState;
 import com.android.server.thread.openthread.DnsTxtAttribute;
 import com.android.server.thread.openthread.IChannelMasksReceiver;
@@ -191,6 +192,7 @@
 
     private final Context mContext;
     private final Handler mHandler;
+    private final MockableSystemProperties mSystemProperties;
 
     // Below member fields can only be accessed from the handler thread (`mHandler`). In
     // particular, the constructor does not run on the handler thread, so it must not touch any of
@@ -235,6 +237,7 @@
     ThreadNetworkControllerService(
             Context context,
             Handler handler,
+            MockableSystemProperties systemProperties,
             NetworkProvider networkProvider,
             Supplier<IOtDaemon> otDaemonSupplier,
             ConnectivityManager connectivityManager,
@@ -249,6 +252,7 @@
             Map<Network, LinkProperties> networkToLinkProperties) {
         mContext = context;
         mHandler = handler;
+        mSystemProperties = systemProperties;
         mNetworkProvider = networkProvider;
         mOtDaemonSupplier = otDaemonSupplier;
         mConnectivityManager = connectivityManager;
@@ -286,6 +290,7 @@
         return new ThreadNetworkControllerService(
                 context,
                 handler,
+                new MockableSystemProperties(),
                 networkProvider,
                 () -> IOtDaemon.Stub.asInterface(ServiceManagerWrapper.waitForService("ot_daemon")),
                 connectivityManager,
@@ -355,7 +360,7 @@
                 newOtDaemonConfig(mPersistentSettings.getConfiguration()),
                 mTunIfController.getTunFd(),
                 mNsdPublisher,
-                getMeshcopTxtAttributes(mResources.get()),
+                getMeshcopTxtAttributes(mResources.get(), mSystemProperties),
                 mCountryCodeSupplier.get(),
                 FeatureFlags.isTrelEnabled(),
                 mOtDaemonCallbackProxy);
@@ -365,10 +370,37 @@
         return mOtDaemon;
     }
 
+    static String getVendorName(Resources resources, MockableSystemProperties systemProperties) {
+        final String PROP_MANUFACTURER = "ro.product.manufacturer";
+        String vendorName = resources.getString(R.string.config_thread_vendor_name);
+        if (vendorName.equalsIgnoreCase(PROP_MANUFACTURER)) {
+            vendorName = systemProperties.get(PROP_MANUFACTURER);
+            // Assume it's always ASCII chars in ro.product.manufacturer
+            if (vendorName.length() > MAX_VENDOR_NAME_UTF8_BYTES) {
+                vendorName = vendorName.substring(0, MAX_VENDOR_NAME_UTF8_BYTES);
+            }
+        }
+        return vendorName;
+    }
+
+    static String getModelName(Resources resources, MockableSystemProperties systemProperties) {
+        final String PROP_MODEL = "ro.product.model";
+        String modelName = resources.getString(R.string.config_thread_model_name);
+        if (modelName.equalsIgnoreCase(PROP_MODEL)) {
+            modelName = systemProperties.get(PROP_MODEL);
+            // Assume it's always ASCII chars in ro.product.model
+            if (modelName.length() > MAX_MODEL_NAME_UTF8_BYTES) {
+                modelName = modelName.substring(0, MAX_MODEL_NAME_UTF8_BYTES);
+            }
+        }
+        return modelName;
+    }
+
     @VisibleForTesting
-    static MeshcopTxtAttributes getMeshcopTxtAttributes(Resources resources) {
-        final String modelName = resources.getString(R.string.config_thread_model_name);
-        final String vendorName = resources.getString(R.string.config_thread_vendor_name);
+    static MeshcopTxtAttributes getMeshcopTxtAttributes(
+            Resources resources, MockableSystemProperties systemProperties) {
+        final String vendorName = getVendorName(resources, systemProperties);
+        final String modelName = getModelName(resources, systemProperties);
         final String vendorOui = resources.getString(R.string.config_thread_vendor_oui);
         final String[] vendorSpecificTxts =
                 resources.getStringArray(R.array.config_thread_mdns_vendor_specific_txts);
@@ -637,6 +669,8 @@
                 .setSrpServerWaitForBorderRoutingEnabled(srpServerWaitEnabled)
                 .setBorderRouterAutoJoinEnabled(autoJoinEnabled)
                 .setCountryCodeEnabled(countryCodeEnabled)
+                .setVendorName(getVendorName(mResources.get(), mSystemProperties))
+                .setModelName(getModelName(mResources.get(), mSystemProperties))
                 .build();
     }
 
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/ServiceDiscoveryTest.java b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
index c4e373a..a9c0da2 100644
--- a/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
+++ b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
@@ -48,6 +48,7 @@
 import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
 import android.net.thread.utils.ThreadNetworkControllerWrapper;
 import android.os.HandlerThread;
+import android.os.SystemProperties;
 
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.filters.LargeTest;
@@ -454,14 +455,22 @@
     }
 
     @Test
-    public void meshcopOverlay_vendorAndModelNameAreSetToOverlayValue() throws Exception {
+    public void meshcopOverlay_vendorAndModelNameAreSetToSystemProperties() throws Exception {
         NsdServiceInfo discoveredService = discoverService(mNsdManager, "_meshcop._udp");
         assertThat(discoveredService).isNotNull();
         NsdServiceInfo meshcopService = resolveService(mNsdManager, discoveredService);
+        String expectedVendorName = SystemProperties.get("ro.product.manufacturer");
+        if (expectedVendorName.length() > 24) {
+            expectedVendorName = expectedVendorName.substring(0, 24);
+        }
+        String expectedModelName = SystemProperties.get("ro.product.model");
+        if (expectedModelName.length() > 24) {
+            expectedModelName = expectedModelName.substring(0, 24);
+        }
 
         Map<String, byte[]> txtMap = meshcopService.getAttributes();
-        assertThat(txtMap.get("vn")).isEqualTo("Android".getBytes(UTF_8));
-        assertThat(txtMap.get("mn")).isEqualTo("Thread Border Router".getBytes(UTF_8));
+        assertThat(txtMap.get("vn")).isEqualTo(expectedVendorName.getBytes(UTF_8));
+        assertThat(txtMap.get("mn")).isEqualTo(expectedModelName.getBytes(UTF_8));
     }
 
     @Test
diff --git a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
index b608c5d..f586f6e 100644
--- a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
+++ b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
@@ -24,9 +24,13 @@
 import static android.net.thread.utils.IntegrationTestUtils.getIpv6Addresses;
 import static android.net.thread.utils.IntegrationTestUtils.getIpv6LinkAddresses;
 import static android.net.thread.utils.IntegrationTestUtils.waitFor;
+import static android.net.thread.utils.OtDaemonController.DIAG_VENDOR_MODEL_TLV_TYPE;
+import static android.net.thread.utils.OtDaemonController.DIAG_VENDOR_NAME_TLV_TYPE;
 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;
@@ -121,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();
@@ -290,6 +300,17 @@
         // TODO: b/376217403 - enables / disables Border Agent at runtime
     }
 
+    @Test
+    public void networkDiagnostic_vendorAndModelNameAreSet() throws Exception {
+        mController.joinAndWait(DEFAULT_DATASET);
+
+        var tlvTypes = List.of(DIAG_VENDOR_NAME_TLV_TYPE, DIAG_VENDOR_MODEL_TLV_TYPE);
+        var result = mOtCtl.netDiagGet(mOtCtl.getMlEid(), tlvTypes);
+
+        assertThat(result.get("Vendor Name")).isNotEmpty();
+        assertThat(result.get("Vendor Model")).isNotEmpty();
+    }
+
     private NetworkCapabilities registerNetworkCallbackAndWait(NetworkRequest request)
             throws Exception {
         CompletableFuture<Network> networkFuture = new CompletableFuture<>();
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
index 63d6130..a73390c 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -34,6 +34,7 @@
 import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
 import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_TESTING;
 
+import static com.android.server.thread.ThreadNetworkControllerService.getMeshcopTxtAttributes;
 import static com.android.server.thread.ThreadNetworkCountryCode.DEFAULT_COUNTRY_CODE;
 import static com.android.server.thread.ThreadPersistentSettings.KEY_THREAD_ENABLED;
 import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_INVALID_STATE;
@@ -49,8 +50,6 @@
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.isNotNull;
-import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
@@ -101,10 +100,10 @@
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
 import com.android.net.module.util.RoutingCoordinatorManager;
 import com.android.server.connectivity.ConnectivityResources;
+import com.android.server.connectivity.MockableSystemProperties;
 import com.android.server.thread.openthread.DnsTxtAttribute;
 import com.android.server.thread.openthread.IOtStatusReceiver;
 import com.android.server.thread.openthread.MeshcopTxtAttributes;
-import com.android.server.thread.openthread.OtDaemonConfiguration;
 import com.android.server.thread.openthread.testing.FakeOtDaemon;
 
 import org.junit.After;
@@ -181,6 +180,7 @@
     private static final String TEST_MODEL_NAME = "test model";
     private static final LinkAddress TEST_NAT64_CIDR = new LinkAddress("192.168.255.0/24");
 
+    @Mock private MockableSystemProperties mMockSystemProperties;
     @Mock private ConnectivityManager mMockConnectivityManager;
     @Mock private RoutingCoordinatorManager mMockRoutingCoordinatorManager;
     @Mock private NetworkAgent mMockNetworkAgent;
@@ -271,6 +271,7 @@
                 new ThreadNetworkControllerService(
                         mContext,
                         handler,
+                        mMockSystemProperties,
                         networkProvider,
                         () -> mFakeOtDaemon,
                         mMockConnectivityManager,
@@ -336,6 +337,23 @@
     }
 
     @Test
+    public void initialize_vendorAndModelNameSetToProperty_propertiesAreSetToOtDaemon()
+            throws Exception {
+        when(mMockSystemProperties.get(eq("ro.product.manufacturer"))).thenReturn("Banana");
+        when(mResources.getString(eq(R.string.config_thread_vendor_name)))
+                .thenReturn("ro.product.manufacturer");
+        when(mMockSystemProperties.get(eq("ro.product.model"))).thenReturn("Orange");
+        when(mResources.getString(eq(R.string.config_thread_model_name)))
+                .thenReturn("ro.product.model");
+
+        mService.initialize();
+        mTestLooper.dispatchAll();
+
+        assertThat(mFakeOtDaemon.getConfiguration().vendorName).isEqualTo("Banana");
+        assertThat(mFakeOtDaemon.getConfiguration().modelName).isEqualTo("Orange");
+    }
+
+    @Test
     public void initialize_nat64Disabled_doesNotRequestNat64CidrAndConfiguresOtDaemon()
             throws Exception {
         ThreadConfiguration config =
@@ -345,8 +363,7 @@
         mTestLooper.dispatchAll();
 
         verify(mMockRoutingCoordinatorManager, never()).requestDownstreamAddress(any());
-        verify(mFakeOtDaemon, times(1)).setNat64Cidr(isNull(), any());
-        verify(mFakeOtDaemon, never()).setNat64Cidr(isNotNull(), any());
+        assertThat(mFakeOtDaemon.getNat64Cidr()).isNull();
     }
 
     @Test
@@ -359,11 +376,8 @@
         mTestLooper.dispatchAll();
 
         verify(mMockRoutingCoordinatorManager, times(1)).requestDownstreamAddress(any());
-        verify(mFakeOtDaemon, times(1))
-                .setConfiguration(
-                        new OtDaemonConfiguration.Builder().setNat64Enabled(true).build(),
-                        null /* receiver */);
-        verify(mFakeOtDaemon, times(1)).setNat64Cidr(eq(TEST_NAT64_CIDR.toString()), any());
+        assertThat(mFakeOtDaemon.getConfiguration().nat64Enabled).isTrue();
+        assertThat(mFakeOtDaemon.getNat64Cidr()).isEqualTo(TEST_NAT64_CIDR.toString());
     }
 
     @Test
@@ -400,7 +414,7 @@
         when(mResources.getString(eq(R.string.config_thread_vendor_name))).thenReturn("");
 
         MeshcopTxtAttributes meshcopTxts =
-                ThreadNetworkControllerService.getMeshcopTxtAttributes(mResources);
+                getMeshcopTxtAttributes(mResources, mMockSystemProperties);
 
         assertThat(meshcopTxts.vendorName).isEqualTo("");
     }
@@ -412,7 +426,31 @@
 
         assertThrows(
                 IllegalStateException.class,
-                () -> ThreadNetworkControllerService.getMeshcopTxtAttributes(mResources));
+                () -> getMeshcopTxtAttributes(mResources, mMockSystemProperties));
+    }
+
+    @Test
+    public void getMeshcopTxtAttributes_VendorNameSetToManufacturer_manufacturerPropertyIsUsed() {
+        when(mMockSystemProperties.get(eq("ro.product.manufacturer"))).thenReturn("Banana");
+        when(mResources.getString(eq(R.string.config_thread_vendor_name)))
+                .thenReturn("ro.product.manufacturer");
+
+        MeshcopTxtAttributes meshcopTxts =
+                getMeshcopTxtAttributes(mResources, mMockSystemProperties);
+
+        assertThat(meshcopTxts.vendorName).isEqualTo("Banana");
+    }
+
+    @Test
+    public void getMeshcopTxtAttributes_ModelNameSetToModelProperty_modelPropertyIsUsed() {
+        when(mMockSystemProperties.get(eq("ro.product.model"))).thenReturn("Orange");
+        when(mResources.getString(eq(R.string.config_thread_model_name)))
+                .thenReturn("ro.product.model");
+
+        MeshcopTxtAttributes meshcopTxts =
+                getMeshcopTxtAttributes(mResources, mMockSystemProperties);
+
+        assertThat(meshcopTxts.modelName).isEqualTo("Orange");
     }
 
     @Test
@@ -422,14 +460,14 @@
 
         assertThrows(
                 IllegalStateException.class,
-                () -> ThreadNetworkControllerService.getMeshcopTxtAttributes(mResources));
+                () -> getMeshcopTxtAttributes(mResources, mMockSystemProperties));
     }
 
     @Test
     public void getMeshcopTxtAttributes_emptyModelName_accepted() {
         when(mResources.getString(eq(R.string.config_thread_model_name))).thenReturn("");
 
-        var meshcopTxts = ThreadNetworkControllerService.getMeshcopTxtAttributes(mResources);
+        var meshcopTxts = getMeshcopTxtAttributes(mResources, mMockSystemProperties);
         assertThat(meshcopTxts.modelName).isEqualTo("");
     }
 
@@ -461,7 +499,7 @@
 
     private byte[] getMeshcopTxtAttributesWithVendorOui(String vendorOui) {
         when(mResources.getString(eq(R.string.config_thread_vendor_oui))).thenReturn(vendorOui);
-        return ThreadNetworkControllerService.getMeshcopTxtAttributes(mResources).vendorOui;
+        return getMeshcopTxtAttributes(mResources, mMockSystemProperties).vendorOui;
     }
 
     @Test
@@ -886,16 +924,12 @@
 
         verify(mockReceiver, times(1)).onSuccess();
         verify(mMockRoutingCoordinatorManager, times(1)).requestDownstreamAddress(any());
-        verify(mFakeOtDaemon, times(1))
-                .setConfiguration(
-                        eq(new OtDaemonConfiguration.Builder().setNat64Enabled(true).build()),
-                        any(IOtStatusReceiver.class));
-        verify(mFakeOtDaemon, times(1))
-                .setNat64Cidr(eq(TEST_NAT64_CIDR.toString()), any(IOtStatusReceiver.class));
+        assertThat(mFakeOtDaemon.getConfiguration().nat64Enabled).isTrue();
+        assertThat(mFakeOtDaemon.getNat64Cidr()).isEqualTo(TEST_NAT64_CIDR.toString());
     }
 
     @Test
-    public void setConfiguration_enablesNat64_otDaemonRemoteFailure_serviceDoesNotCrash()
+    public void setConfiguration_enablesNat64AndOtDaemonRemoteFailure_serviceDoesNotCrash()
             throws Exception {
         mService.initialize();
         mTestLooper.dispatchAll();
@@ -929,12 +963,8 @@
         verify(mockReceiver, times(1)).onSuccess();
         verify(mMockRoutingCoordinatorManager, times(1)).releaseDownstream(any());
         verify(mMockRoutingCoordinatorManager, never()).requestDownstreamAddress(any());
-        verify(mFakeOtDaemon, times(1))
-                .setConfiguration(
-                        eq(new OtDaemonConfiguration.Builder().setNat64Enabled(false).build()),
-                        any(IOtStatusReceiver.class));
-        verify(mFakeOtDaemon, times(1)).setNat64Cidr(isNull(), any(IOtStatusReceiver.class));
-        verify(mFakeOtDaemon, never()).setNat64Cidr(isNotNull(), any(IOtStatusReceiver.class));
+        assertThat(mFakeOtDaemon.getConfiguration().nat64Enabled).isFalse();
+        assertThat(mFakeOtDaemon.getNat64Cidr()).isNull();
     }
 
     @Test
diff --git a/thread/tests/utils/src/android/net/thread/utils/OtDaemonController.java b/thread/tests/utils/src/android/net/thread/utils/OtDaemonController.java
index d35b94e..41d9eaf 100644
--- a/thread/tests/utils/src/android/net/thread/utils/OtDaemonController.java
+++ b/thread/tests/utils/src/android/net/thread/utils/OtDaemonController.java
@@ -24,10 +24,13 @@
 import com.android.compatibility.common.util.SystemUtil;
 
 import java.net.Inet6Address;
+import java.net.InetAddress;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.stream.Collectors;
 
 /**
@@ -37,6 +40,9 @@
  * <p>Note that this class takes root privileged to run.
  */
 public final class OtDaemonController {
+    public static final int DIAG_VENDOR_NAME_TLV_TYPE = 25;
+    public static final int DIAG_VENDOR_MODEL_TLV_TYPE = 26;
+
     private static final String OT_CTL = "/system/bin/ot-ctl";
 
     /**
@@ -188,6 +194,35 @@
     }
 
     /**
+     * Sends DIAG_GET request to the given peer device and returns the parsed result as a dict of
+     * the requested TLV values.
+     *
+     * <p>For example, a request {@code netDiagGet("fdad:3d13:7b11:4049:ed1a:7e87:4770:a345",
+     * [DIAG_VENDOR_NAME_TLV_TYPE, DIAG_VENDOR_MODEL_TLV_TYPE])} can return a dict of {@code
+     * {"Vendor Name" : "ABC", "Vendor Model" : "Cuttlefish"}}
+     */
+    public Map<String, String> netDiagGet(InetAddress peerAddr, List<Integer> tlvTypes) {
+        String tlvTypeList =
+                tlvTypes.stream().map(Object::toString).collect(Collectors.joining(" "));
+
+        List<String> outputs =
+                executeCommandAndParse(
+                        "networkdiagnostic get " + peerAddr.getHostAddress() + " " + tlvTypeList);
+        Map<String, String> result = new HashMap<>();
+        for (String line : outputs) {
+            if (line.startsWith("DIAG_GET")) {
+                continue;
+            }
+            String[] keyValue = line.split(":");
+            if (keyValue.length != 2) {
+                throw new IllegalStateException("Unexpected OT output: " + line);
+            }
+            result.put(keyValue[0].strip(), keyValue[1].strip());
+        }
+        return result;
+    }
+
+    /**
      * Executes a ot-ctl command and parse the output to a list of strings.
      *
      * <p>The trailing "Done" in the command output will be dropped.
diff --git a/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java b/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java
index 9a1a05b..62c2785 100644
--- a/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java
+++ b/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java
@@ -16,44 +16,23 @@
 package android.net.thread.utils;
 
 import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
-import static android.net.InetAddresses.parseNumericAddress;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
-import static android.net.NetworkCapabilities.TRANSPORT_TEST;
-import static android.system.OsConstants.AF_INET6;
-import static android.system.OsConstants.IPPROTO_UDP;
-import static android.system.OsConstants.SOCK_DGRAM;
 
 import static com.android.testutils.RecorderCallback.CallbackEntry.LINK_PROPERTIES_CHANGED;
 import static com.android.testutils.TestPermissionUtil.runAsShell;
 
 import android.content.Context;
 import android.net.ConnectivityManager;
-import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.Network;
-import android.net.NetworkAgentConfig;
-import android.net.NetworkCapabilities;
-import android.net.NetworkRequest;
 import android.net.TestNetworkInterface;
 import android.net.TestNetworkManager;
-import android.net.TestNetworkSpecifier;
 import android.os.Looper;
-import android.system.ErrnoException;
-import android.system.Os;
 
-import com.android.compatibility.common.util.PollingCheck;
 import com.android.testutils.TestableNetworkAgent;
 import com.android.testutils.TestableNetworkCallback;
 
-import java.io.FileDescriptor;
 import java.io.IOException;
-import java.net.InterfaceAddress;
-import java.net.NetworkInterface;
-import java.net.SocketException;
 import java.time.Duration;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
 
 /** A class that can create/destroy a test network based on TAP interface. */
 public final class TapTestNetworkTracker {