Merge "Revert "Add SkipNativeNetworkCreation flag in NetworkAgentConfig"" into main
diff --git a/OWNERS_core_networking b/OWNERS_core_networking
index 078ccde..4b73639 100644
--- a/OWNERS_core_networking
+++ b/OWNERS_core_networking
@@ -1,4 +1,5 @@
 jchalard@google.com
+jimictw@google.com
 junyulai@google.com
 lorenzo@google.com
 maze@google.com
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index 39009cb..c301397 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -1,15 +1,13 @@
 [Builtin Hooks]
 bpfmt = true
 clang_format = true
-ktfmt = true
 
 [Builtin Hooks Options]
 clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp,hpp
-ktfmt = --kotlinlang-style
 
 [Hook Scripts]
 checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
 
-ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py --no-verify-format -f ${PREUPLOAD_FILES}
+ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py --disabled-rules comment-wrapping -f ${PREUPLOAD_FILES}
 
 hidden_api_txt_checksorted_hook = ${REPO_ROOT}/tools/platform-compat/hiddenapi/checksorted_sha.sh ${PREUPLOAD_COMMIT} ${REPO_ROOT}
diff --git a/TEST_MAPPING b/TEST_MAPPING
index c1bc31e..8592af2 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -77,7 +77,7 @@
       "name": "libnetworkstats_test"
     },
     {
-      "name": "CtsTetheringTestLatestSdk",
+      "name": "CtsTetheringTest",
       "options": [
         {
           "exclude-annotation": "com.android.testutils.NetworkStackModuleTest"
@@ -226,6 +226,9 @@
     },
     {
       "name": "FrameworksNetIntegrationTests"
+    },
+    {
+      "name": "CtsTetheringTest"
     }
   ],
   "postsubmit": [
@@ -391,7 +394,7 @@
       "name": "libnetworkstats_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
     },
     {
-      "name": "CtsTetheringTestLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+      "name": "CtsTetheringTest[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
       "options": [
         {
           "exclude-annotation": "com.android.testutils.NetworkStackModuleTest"
@@ -406,7 +409,7 @@
       "keywords": ["sim"]
     },
     {
-      "name": "CtsTetheringTestLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+      "name": "CtsTetheringTest[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
       "keywords": ["sim"],
       "options": [
         {
@@ -427,6 +430,9 @@
   "automotive-mumd-presubmit": [
     {
       "name": "CtsNetTestCases"
+    },
+    {
+      "name": "CtsNetTestCasesUpdateStatsPermission"
     }
   ],
   "imports": [
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 5cf5528..091849b 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -58,11 +58,13 @@
         ":framework-connectivity-shared-srcs",
         ":services-tethering-shared-srcs",
         ":statslog-connectivity-java-gen",
+        ":statslog-framework-connectivity-java-gen",
         ":statslog-tethering-java-gen",
     ],
     static_libs: [
         "androidx.annotation_annotation",
         "connectivity-net-module-utils-bpf",
+        "com.android.net.flags-aconfig-java",
         "modules-utils-build",
         "modules-utils-statemachine",
         "networkstack-client",
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 0c05354..19dd492 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -155,7 +155,10 @@
         "framework-connectivity",
         "framework-connectivity-t",
         "framework-tethering",
-    ],
+    ] + select(release_flag("RELEASE_MOVE_VCN_TO_MAINLINE"), {
+        true: ["framework-connectivity-b"],
+        default: [],
+    }),
     apex_available: ["com.android.tethering"],
 
     // The bootclasspath_fragments that provide APIs on which this depends.
@@ -195,6 +198,7 @@
             "android.net.http",
             "android.net.netstats",
             "android.net.util",
+            "android.net.vcn",
         ],
 
         // The following packages and all their subpackages currently only
diff --git a/Tethering/common/TetheringLib/api/current.txt b/Tethering/common/TetheringLib/api/current.txt
index d802177..932e801 100644
--- a/Tethering/common/TetheringLib/api/current.txt
+++ b/Tethering/common/TetheringLib/api/current.txt
@@ -1 +1,71 @@
 // Signature format: 2.0
+package android.net {
+
+  public final class TetheringInterface implements android.os.Parcelable {
+    ctor public TetheringInterface(int, @NonNull String);
+    ctor @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") public TetheringInterface(int, @NonNull String, @Nullable android.net.wifi.SoftApConfiguration);
+    method public int describeContents();
+    method @NonNull public String getInterface();
+    method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @Nullable @RequiresPermission(value=android.Manifest.permission.NETWORK_SETTINGS, conditional=true) public android.net.wifi.SoftApConfiguration getSoftApConfiguration();
+    method public int getType();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.TetheringInterface> CREATOR;
+  }
+
+  public class TetheringManager {
+    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerTetheringEventCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.TetheringEventCallback);
+    method @RequiresPermission(value=android.Manifest.permission.TETHER_PRIVILEGED, conditional=true) public void startTethering(@NonNull android.net.TetheringManager.TetheringRequest, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.StartTetheringCallback);
+    method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @RequiresPermission(value=android.Manifest.permission.TETHER_PRIVILEGED, conditional=true) public void stopTethering(@NonNull android.net.TetheringManager.TetheringRequest, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.StopTetheringCallback);
+    method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.ACCESS_NETWORK_STATE}) public void unregisterTetheringEventCallback(@NonNull android.net.TetheringManager.TetheringEventCallback);
+    field public static final int CONNECTIVITY_SCOPE_GLOBAL = 1; // 0x1
+    field public static final int TETHERING_WIFI = 0; // 0x0
+    field public static final int TETHER_ERROR_DHCPSERVER_ERROR = 12; // 0xc
+    field public static final int TETHER_ERROR_DISABLE_FORWARDING_ERROR = 9; // 0x9
+    field @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") public static final int TETHER_ERROR_DUPLICATE_REQUEST = 18; // 0x12
+    field public static final int TETHER_ERROR_ENABLE_FORWARDING_ERROR = 8; // 0x8
+    field public static final int TETHER_ERROR_ENTITLEMENT_UNKNOWN = 13; // 0xd
+    field public static final int TETHER_ERROR_IFACE_CFG_ERROR = 10; // 0xa
+    field public static final int TETHER_ERROR_INTERNAL_ERROR = 5; // 0x5
+    field public static final int TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION = 15; // 0xf
+    field public static final int TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION = 14; // 0xe
+    field public static final int TETHER_ERROR_NO_ERROR = 0; // 0x0
+    field public static final int TETHER_ERROR_PROVISIONING_FAILED = 11; // 0xb
+    field public static final int TETHER_ERROR_SERVICE_UNAVAIL = 2; // 0x2
+    field public static final int TETHER_ERROR_TETHER_IFACE_ERROR = 6; // 0x6
+    field public static final int TETHER_ERROR_UNAVAIL_IFACE = 4; // 0x4
+    field public static final int TETHER_ERROR_UNKNOWN_IFACE = 1; // 0x1
+    field @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") public static final int TETHER_ERROR_UNKNOWN_REQUEST = 17; // 0x11
+    field public static final int TETHER_ERROR_UNKNOWN_TYPE = 16; // 0x10
+    field public static final int TETHER_ERROR_UNSUPPORTED = 3; // 0x3
+    field public static final int TETHER_ERROR_UNTETHER_IFACE_ERROR = 7; // 0x7
+  }
+
+  public static interface TetheringManager.StartTetheringCallback {
+    method public default void onTetheringFailed(int);
+    method public default void onTetheringStarted();
+  }
+
+  @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") public static interface TetheringManager.StopTetheringCallback {
+    method public default void onStopTetheringFailed(int);
+    method public default void onStopTetheringSucceeded();
+  }
+
+  public static interface TetheringManager.TetheringEventCallback {
+    method public default void onTetheredInterfacesChanged(@NonNull java.util.Set<android.net.TetheringInterface>);
+  }
+
+  public static final class TetheringManager.TetheringRequest implements android.os.Parcelable {
+    method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") public int describeContents();
+    method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @Nullable public android.net.wifi.SoftApConfiguration getSoftApConfiguration();
+    method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @NonNull public static final android.os.Parcelable.Creator<android.net.TetheringManager.TetheringRequest> CREATOR;
+  }
+
+  public static class TetheringManager.TetheringRequest.Builder {
+    ctor public TetheringManager.TetheringRequest.Builder(int);
+    method @NonNull public android.net.TetheringManager.TetheringRequest build();
+    method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setSoftApConfiguration(@Nullable android.net.wifi.SoftApConfiguration);
+  }
+
+}
+
diff --git a/Tethering/common/TetheringLib/api/module-lib-current.txt b/Tethering/common/TetheringLib/api/module-lib-current.txt
index e893894..3ba8e1b 100644
--- a/Tethering/common/TetheringLib/api/module-lib-current.txt
+++ b/Tethering/common/TetheringLib/api/module-lib-current.txt
@@ -22,7 +22,7 @@
     method public boolean isTetheringSupported(@NonNull String);
     method public void requestLatestTetheringEntitlementResult(int, @NonNull android.os.ResultReceiver, boolean);
     method @Deprecated public int setUsbTethering(boolean);
-    method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void startTethering(int, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.StartTetheringCallback);
+    method @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void startTethering(int, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.StartTetheringCallback);
     method @Deprecated public int tether(@NonNull String);
     method @Deprecated public int untether(@NonNull String);
   }
@@ -47,9 +47,14 @@
   }
 
   public static final class TetheringManager.TetheringRequest implements android.os.Parcelable {
+    method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @Nullable public String getInterfaceName();
     method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @Nullable public String getPackageName();
     method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") public int getUid();
   }
 
+  public static class TetheringManager.TetheringRequest.Builder {
+    method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @NonNull @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public android.net.TetheringManager.TetheringRequest.Builder setInterfaceName(@Nullable String);
+  }
+
 }
 
diff --git a/Tethering/common/TetheringLib/api/system-current.txt b/Tethering/common/TetheringLib/api/system-current.txt
index 0e85956..c0c0abc 100644
--- a/Tethering/common/TetheringLib/api/system-current.txt
+++ b/Tethering/common/TetheringLib/api/system-current.txt
@@ -19,26 +19,11 @@
     field @NonNull public static final android.os.Parcelable.Creator<android.net.TetheredClient.AddressInfo> CREATOR;
   }
 
-  public final class TetheringInterface implements android.os.Parcelable {
-    ctor public TetheringInterface(int, @NonNull String);
-    ctor @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") public TetheringInterface(int, @NonNull String, @Nullable android.net.wifi.SoftApConfiguration);
-    method public int describeContents();
-    method @NonNull public String getInterface();
-    method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @Nullable @RequiresPermission(value=android.Manifest.permission.NETWORK_SETTINGS, conditional=true) public android.net.wifi.SoftApConfiguration getSoftApConfiguration();
-    method public int getType();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.net.TetheringInterface> CREATOR;
-  }
-
   public class TetheringManager {
-    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerTetheringEventCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.TetheringEventCallback);
-    method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void requestLatestTetheringEntitlementResult(int, boolean, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.OnTetheringEntitlementResultListener);
-    method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void startTethering(@NonNull android.net.TetheringManager.TetheringRequest, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.StartTetheringCallback);
-    method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void stopAllTethering();
-    method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void stopTethering(int);
-    method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.ACCESS_NETWORK_STATE}) public void unregisterTetheringEventCallback(@NonNull android.net.TetheringManager.TetheringEventCallback);
+    method @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void requestLatestTetheringEntitlementResult(int, boolean, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.OnTetheringEntitlementResultListener);
+    method @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void stopAllTethering();
+    method @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void stopTethering(int);
     field @Deprecated public static final String ACTION_TETHER_STATE_CHANGED = "android.net.conn.TETHER_STATE_CHANGED";
-    field public static final int CONNECTIVITY_SCOPE_GLOBAL = 1; // 0x1
     field public static final int CONNECTIVITY_SCOPE_LOCAL = 2; // 0x2
     field public static final String EXTRA_ACTIVE_LOCAL_ONLY = "android.net.extra.ACTIVE_LOCAL_ONLY";
     field public static final String EXTRA_ACTIVE_TETHER = "tetherArray";
@@ -50,25 +35,7 @@
     field public static final int TETHERING_NCM = 4; // 0x4
     field public static final int TETHERING_USB = 1; // 0x1
     field @FlaggedApi("com.android.net.flags.tethering_request_virtual") public static final int TETHERING_VIRTUAL = 7; // 0x7
-    field public static final int TETHERING_WIFI = 0; // 0x0
     field public static final int TETHERING_WIFI_P2P = 3; // 0x3
-    field public static final int TETHER_ERROR_DHCPSERVER_ERROR = 12; // 0xc
-    field public static final int TETHER_ERROR_DISABLE_FORWARDING_ERROR = 9; // 0x9
-    field public static final int TETHER_ERROR_ENABLE_FORWARDING_ERROR = 8; // 0x8
-    field public static final int TETHER_ERROR_ENTITLEMENT_UNKNOWN = 13; // 0xd
-    field public static final int TETHER_ERROR_IFACE_CFG_ERROR = 10; // 0xa
-    field public static final int TETHER_ERROR_INTERNAL_ERROR = 5; // 0x5
-    field public static final int TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION = 15; // 0xf
-    field public static final int TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION = 14; // 0xe
-    field public static final int TETHER_ERROR_NO_ERROR = 0; // 0x0
-    field public static final int TETHER_ERROR_PROVISIONING_FAILED = 11; // 0xb
-    field public static final int TETHER_ERROR_SERVICE_UNAVAIL = 2; // 0x2
-    field public static final int TETHER_ERROR_TETHER_IFACE_ERROR = 6; // 0x6
-    field public static final int TETHER_ERROR_UNAVAIL_IFACE = 4; // 0x4
-    field public static final int TETHER_ERROR_UNKNOWN_IFACE = 1; // 0x1
-    field public static final int TETHER_ERROR_UNKNOWN_TYPE = 16; // 0x10
-    field public static final int TETHER_ERROR_UNSUPPORTED = 3; // 0x3
-    field public static final int TETHER_ERROR_UNTETHER_IFACE_ERROR = 7; // 0x7
     field public static final int TETHER_HARDWARE_OFFLOAD_FAILED = 2; // 0x2
     field public static final int TETHER_HARDWARE_OFFLOAD_STARTED = 1; // 0x1
     field public static final int TETHER_HARDWARE_OFFLOAD_STOPPED = 0; // 0x0
@@ -78,11 +45,6 @@
     method public void onTetheringEntitlementResult(int);
   }
 
-  public static interface TetheringManager.StartTetheringCallback {
-    method public default void onTetheringFailed(int);
-    method public default void onTetheringStarted();
-  }
-
   public static interface TetheringManager.TetheringEventCallback {
     method public default void onClientsChanged(@NonNull java.util.Collection<android.net.TetheredClient>);
     method public default void onError(@NonNull String, int);
@@ -93,31 +55,23 @@
     method public default void onTetherableInterfacesChanged(@NonNull java.util.List<java.lang.String>);
     method public default void onTetherableInterfacesChanged(@NonNull java.util.Set<android.net.TetheringInterface>);
     method public default void onTetheredInterfacesChanged(@NonNull java.util.List<java.lang.String>);
-    method public default void onTetheredInterfacesChanged(@NonNull java.util.Set<android.net.TetheringInterface>);
     method public default void onTetheringSupported(boolean);
     method public default void onUpstreamChanged(@Nullable android.net.Network);
   }
 
   public static final class TetheringManager.TetheringRequest implements android.os.Parcelable {
-    method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") public int describeContents();
     method @Nullable public android.net.LinkAddress getClientStaticIpv4Address();
     method public int getConnectivityScope();
     method @Nullable public android.net.LinkAddress getLocalIpv4Address();
     method public boolean getShouldShowEntitlementUi();
-    method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @Nullable public android.net.wifi.SoftApConfiguration getSoftApConfiguration();
     method public int getTetheringType();
     method public boolean isExemptFromEntitlementCheck();
-    method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @NonNull public static final android.os.Parcelable.Creator<android.net.TetheringManager.TetheringRequest> CREATOR;
   }
 
   public static class TetheringManager.TetheringRequest.Builder {
-    ctor public TetheringManager.TetheringRequest.Builder(int);
-    method @NonNull public android.net.TetheringManager.TetheringRequest build();
     method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setConnectivityScope(int);
     method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setExemptFromEntitlementCheck(boolean);
     method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setShouldShowEntitlementUi(boolean);
-    method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setSoftApConfiguration(@Nullable android.net.wifi.SoftApConfiguration);
     method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setStaticIpv4Addresses(@NonNull android.net.LinkAddress, @NonNull android.net.LinkAddress);
   }
 
diff --git a/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl b/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl
index 77e78bd..7d244e2 100644
--- a/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl
+++ b/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl
@@ -18,6 +18,7 @@
 import android.net.IIntResultListener;
 import android.net.ITetheringEventCallback;
 import android.net.TetheringRequestParcel;
+import android.net.TetheringManager.TetheringRequest;
 import android.os.ResultReceiver;
 
 /** @hide */
@@ -37,6 +38,9 @@
     void stopTethering(int type, String callerPkg, String callingAttributionTag,
             IIntResultListener receiver);
 
+    void stopTetheringRequest(in TetheringRequest request, String callerPkg,
+            String callingAttributionTag, IIntResultListener receiver);
+
     void requestLatestTetheringEntitlementResult(int type, in ResultReceiver receiver,
             boolean showEntitlementUi, String callerPkg, String callingAttributionTag);
 
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringInterface.java b/Tethering/common/TetheringLib/src/android/net/TetheringInterface.java
index 0464fe0..250179a 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringInterface.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringInterface.java
@@ -21,7 +21,6 @@
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.annotation.SuppressLint;
-import android.annotation.SystemApi;
 import android.net.TetheringManager.TetheringType;
 import android.net.wifi.SoftApConfiguration;
 import android.os.Parcel;
@@ -33,9 +32,8 @@
 
 /**
  * The mapping of tethering interface and type.
- * @hide
  */
-@SystemApi
+@SuppressLint("UnflaggedApi")
 public final class TetheringInterface implements Parcelable {
     private final int mType;
     private final String mInterface;
@@ -57,12 +55,14 @@
     }
 
     /** Get tethering type. */
+    @SuppressLint("UnflaggedApi")
     public int getType() {
         return mType;
     }
 
     /** Get tethering interface. */
     @NonNull
+    @SuppressLint("UnflaggedApi")
     public String getInterface() {
         return mInterface;
     }
@@ -75,11 +75,13 @@
     @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
     @RequiresPermission(value = android.Manifest.permission.NETWORK_SETTINGS, conditional = true)
     @Nullable
+    @SuppressLint("UnflaggedApi")
     public SoftApConfiguration getSoftApConfiguration() {
         return mSoftApConfig;
     }
 
     @Override
+    @SuppressLint("UnflaggedApi")
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeInt(mType);
         dest.writeString(mInterface);
@@ -87,11 +89,13 @@
     }
 
     @Override
+    @SuppressLint("UnflaggedApi")
     public int hashCode() {
         return Objects.hash(mType, mInterface, mSoftApConfig);
     }
 
     @Override
+    @SuppressLint("UnflaggedApi")
     public boolean equals(@Nullable Object obj) {
         if (!(obj instanceof TetheringInterface)) return false;
         final TetheringInterface other = (TetheringInterface) obj;
@@ -100,11 +104,13 @@
     }
 
     @Override
+    @SuppressLint("UnflaggedApi")
     public int describeContents() {
         return 0;
     }
 
     @NonNull
+    @SuppressLint("UnflaggedApi")
     public static final Creator<TetheringInterface> CREATOR = new Creator<TetheringInterface>() {
         @NonNull
         @Override
@@ -116,6 +122,7 @@
 
         @NonNull
         @Override
+        @SuppressLint("UnflaggedApi")
         public TetheringInterface[] newArray(int size) {
             return new TetheringInterface[size];
         }
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index bc771da..0ac97f0 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -28,6 +28,7 @@
 import android.content.Context;
 import android.net.wifi.SoftApConfiguration;
 import android.net.wifi.WifiManager;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.ConditionVariable;
 import android.os.IBinder;
@@ -63,9 +64,8 @@
  * <p> The primary responsibilities of this class are to provide the APIs for applications to
  * start tethering, stop tethering, query configuration and query status.
  *
- * @hide
  */
-@SystemApi
+@SuppressLint({"NotCloseable", "UnflaggedApi"})
 public class TetheringManager {
     private static final String TAG = TetheringManager.class.getSimpleName();
     private static final int DEFAULT_TIMEOUT_MS = 60_000;
@@ -95,36 +95,46 @@
      * {@code TetheringManager.EXTRA_ERRORED_TETHER} to indicate
      * the current state of tethering.  Each include a list of
      * interface names in that state (may be empty).
+     * @hide
      *
      * @deprecated New client should use TetheringEventCallback instead.
      */
     @Deprecated
+    @SystemApi
     public static final String ACTION_TETHER_STATE_CHANGED =
             "android.net.conn.TETHER_STATE_CHANGED";
 
     /**
      * gives a String[] listing all the interfaces configured for
      * tethering and currently available for tethering.
+     * @hide
      */
+    @SystemApi
     public static final String EXTRA_AVAILABLE_TETHER = "availableArray";
 
     /**
      * gives a String[] listing all the interfaces currently in local-only
      * mode (ie, has DHCPv4+IPv6-ULA support and no packet forwarding)
+     * @hide
      */
+    @SystemApi
     public static final String EXTRA_ACTIVE_LOCAL_ONLY = "android.net.extra.ACTIVE_LOCAL_ONLY";
 
     /**
      * gives a String[] listing all the interfaces currently tethered
      * (ie, has DHCPv4 support and packets potentially forwarded/NATed)
+     * @hide
      */
+    @SystemApi
     public static final String EXTRA_ACTIVE_TETHER = "tetherArray";
 
     /**
      * gives a String[] listing all the interfaces we tried to tether and
      * failed.  Use {@link #getLastTetherError} to find the error code
      * for any interfaces listed here.
+     * @hide
      */
+    @SystemApi
     public static final String EXTRA_ERRORED_TETHER = "erroredArray";
 
     /** @hide */
@@ -136,6 +146,7 @@
             TETHERING_WIFI_P2P,
             TETHERING_NCM,
             TETHERING_ETHERNET,
+            TETHERING_VIRTUAL,
     })
     public @interface TetheringType {
     }
@@ -143,44 +154,57 @@
     /**
      * Invalid tethering type.
      * @see #startTethering.
+     * @hide
      */
+    @SystemApi
     public static final int TETHERING_INVALID   = -1;
 
     /**
      * Wifi tethering type.
      * @see #startTethering.
      */
+    @SuppressLint("UnflaggedApi")
     public static final int TETHERING_WIFI      = 0;
 
     /**
      * USB tethering type.
      * @see #startTethering.
+     * @hide
      */
+    @SystemApi
     public static final int TETHERING_USB       = 1;
 
     /**
      * Bluetooth tethering type.
      * @see #startTethering.
+     * @hide
      */
+    @SystemApi
     public static final int TETHERING_BLUETOOTH = 2;
 
     /**
      * Wifi P2p tethering type.
      * Wifi P2p tethering is set through events automatically, and don't
      * need to start from #startTethering.
+     * @hide
      */
+    @SystemApi
     public static final int TETHERING_WIFI_P2P = 3;
 
     /**
      * Ncm local tethering type.
      * @see #startTethering(TetheringRequest, Executor, StartTetheringCallback)
+     * @hide
      */
+    @SystemApi
     public static final int TETHERING_NCM = 4;
 
     /**
      * Ethernet tethering type.
      * @see #startTethering(TetheringRequest, Executor, StartTetheringCallback)
+     * @hide
      */
+    @SystemApi
     public static final int TETHERING_ETHERNET = 5;
 
     /**
@@ -253,30 +277,67 @@
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(value = {
             TETHER_ERROR_SERVICE_UNAVAIL,
+            TETHER_ERROR_UNSUPPORTED,
             TETHER_ERROR_INTERNAL_ERROR,
             TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION,
             TETHER_ERROR_UNKNOWN_TYPE,
+            TETHER_ERROR_DUPLICATE_REQUEST,
     })
     public @interface StartTetheringError {
     }
 
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(value = {
+            TETHER_ERROR_NO_ERROR,
+            TETHER_ERROR_UNKNOWN_REQUEST,
+    })
+    public @interface StopTetheringError {
+    }
+
+    @SuppressLint("UnflaggedApi")
     public static final int TETHER_ERROR_NO_ERROR = 0;
+    @SuppressLint("UnflaggedApi")
     public static final int TETHER_ERROR_UNKNOWN_IFACE = 1;
+    @SuppressLint("UnflaggedApi")
     public static final int TETHER_ERROR_SERVICE_UNAVAIL = 2;
+    @SuppressLint("UnflaggedApi")
     public static final int TETHER_ERROR_UNSUPPORTED = 3;
+    @SuppressLint("UnflaggedApi")
     public static final int TETHER_ERROR_UNAVAIL_IFACE = 4;
+    @SuppressLint("UnflaggedApi")
     public static final int TETHER_ERROR_INTERNAL_ERROR = 5;
+    @SuppressLint("UnflaggedApi")
     public static final int TETHER_ERROR_TETHER_IFACE_ERROR = 6;
+    @SuppressLint("UnflaggedApi")
     public static final int TETHER_ERROR_UNTETHER_IFACE_ERROR = 7;
+    @SuppressLint("UnflaggedApi")
     public static final int TETHER_ERROR_ENABLE_FORWARDING_ERROR = 8;
+    @SuppressLint("UnflaggedApi")
     public static final int TETHER_ERROR_DISABLE_FORWARDING_ERROR = 9;
+    @SuppressLint("UnflaggedApi")
     public static final int TETHER_ERROR_IFACE_CFG_ERROR = 10;
+    @SuppressLint("UnflaggedApi")
     public static final int TETHER_ERROR_PROVISIONING_FAILED = 11;
+    @SuppressLint("UnflaggedApi")
     public static final int TETHER_ERROR_DHCPSERVER_ERROR = 12;
+    @SuppressLint("UnflaggedApi")
     public static final int TETHER_ERROR_ENTITLEMENT_UNKNOWN = 13;
+    @SuppressLint("UnflaggedApi")
     public static final int TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION = 14;
+    @SuppressLint("UnflaggedApi")
     public static final int TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION = 15;
+    @SuppressLint("UnflaggedApi")
     public static final int TETHER_ERROR_UNKNOWN_TYPE = 16;
+    @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
+    public static final int TETHER_ERROR_UNKNOWN_REQUEST = 17;
+    @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
+    public static final int TETHER_ERROR_DUPLICATE_REQUEST = 18;
+    /**
+     * Never used outside Tethering.java.
+     * @hide
+     */
+    public static final int TETHER_ERROR_BLUETOOTH_SERVICE_PENDING = 19;
 
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
@@ -288,11 +349,23 @@
     public @interface TetherOffloadStatus {
     }
 
-    /** Tethering offload status is stopped. */
+    /**
+     * Tethering offload status is stopped.
+     * @hide
+     */
+    @SystemApi
     public static final int TETHER_HARDWARE_OFFLOAD_STOPPED = 0;
-    /** Tethering offload status is started. */
+    /**
+     * Tethering offload status is started.
+     * @hide
+     */
+    @SystemApi
     public static final int TETHER_HARDWARE_OFFLOAD_STARTED = 1;
-    /** Fail to start tethering offload. */
+    /**
+     * Fail to start tethering offload.
+     * @hide
+     */
+    @SystemApi
     public static final int TETHER_HARDWARE_OFFLOAD_FAILED = 2;
 
     /**
@@ -321,7 +394,9 @@
         // up and be sent from a worker thread; later, they are always sent from the caller thread.
         // Considering that it's just oneway binder calls, and ordering is preserved, this seems
         // better than inconsistent behavior persisting after boot.
-        if (connector != null) {
+        // If system server restarted, mConnectorSupplier might temporarily return a stale (i.e.
+        // dead) version of TetheringService.
+        if (connector != null && connector.isBinderAlive()) {
             mConnector = ITetheringConnector.Stub.asInterface(connector);
         } else {
             startPollingForConnector();
@@ -356,9 +431,8 @@
                 } catch (InterruptedException e) {
                     // Not much to do here, the system needs to wait for the connector
                 }
-
                 final IBinder connector = mConnectorSupplier.get();
-                if (connector != null) {
+                if (connector != null && connector.isBinderAlive()) {
                     onTetheringConnected(ITetheringConnector.Stub.asInterface(connector));
                     return;
                 }
@@ -589,6 +663,13 @@
         }
     }
 
+    private void unsupportedAfterV() {
+        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+            throw new UnsupportedOperationException("Not supported after SDK version "
+                    + Build.VERSION_CODES.VANILLA_ICE_CREAM);
+        }
+    }
+
     /**
      * Attempt to tether the named interface.  This will setup a dhcp server
      * on the interface, forward and NAT IP v4 packets and forward DNS requests
@@ -598,8 +679,10 @@
      * access will of course fail until an upstream network interface becomes
      * active.
      *
-     * @deprecated The only usages is PanService. It uses this for legacy reasons
-     * and will migrate away as soon as possible.
+     * @deprecated Legacy tethering API. Callers should instead use
+     *             {@link #startTethering(int, Executor, StartTetheringCallback)}.
+     *             On SDK versions after {@link Build.VERSION_CODES.VANILLA_ICE_CREAM}, this will
+     *             throw an UnsupportedOperationException.
      *
      * @param iface the interface name to tether.
      * @return error a {@code TETHER_ERROR} value indicating success or failure type
@@ -609,6 +692,8 @@
     @Deprecated
     @SystemApi(client = MODULE_LIBRARIES)
     public int tether(@NonNull final String iface) {
+        unsupportedAfterV();
+
         final String callerPkg = mContext.getOpPackageName();
         Log.i(TAG, "tether caller:" + callerPkg);
         final RequestDispatcher dispatcher = new RequestDispatcher();
@@ -632,14 +717,18 @@
     /**
      * Stop tethering the named interface.
      *
-     * @deprecated The only usages is PanService. It uses this for legacy reasons
-     * and will migrate away as soon as possible.
+     * @deprecated Legacy tethering API. Callers should instead use
+     *             {@link #stopTethering(int)}.
+     *             On SDK versions after {@link Build.VERSION_CODES.VANILLA_ICE_CREAM}, this will
+     *             throw an UnsupportedOperationException.
      *
      * {@hide}
      */
     @Deprecated
     @SystemApi(client = MODULE_LIBRARIES)
     public int untether(@NonNull final String iface) {
+        unsupportedAfterV();
+
         final String callerPkg = mContext.getOpPackageName();
         Log.i(TAG, "untether caller:" + callerPkg);
 
@@ -686,11 +775,14 @@
      * Indicates that this tethering connection will provide connectivity beyond this device (e.g.,
      * global Internet access).
      */
+    @SuppressLint("UnflaggedApi")
     public static final int CONNECTIVITY_SCOPE_GLOBAL = 1;
 
     /**
      * Indicates that this tethering connection will only provide local connectivity.
+     * @hide
      */
+    @SystemApi
     public static final int CONNECTIVITY_SCOPE_LOCAL = 2;
 
     /**
@@ -718,7 +810,48 @@
     /**
      *  Use with {@link #startTethering} to specify additional parameters when starting tethering.
      */
+    @SuppressLint("UnflaggedApi")
     public static final class TetheringRequest implements Parcelable {
+        /**
+         * Tethering started by an explicit call to startTethering.
+         * @hide
+         */
+        public static final int REQUEST_TYPE_EXPLICIT = 0;
+
+        /**
+         * Tethering implicitly started by broadcasts (LOHS and P2P). Can never be pending.
+         * @hide
+         */
+        public static final int REQUEST_TYPE_IMPLICIT = 1;
+
+        /**
+         * Tethering started by the legacy tether() call. Can only happen on V-.
+         * @hide
+         */
+        public static final int REQUEST_TYPE_LEGACY = 2;
+
+        /**
+         * Tethering started but there was no pending request found. This may happen if Tethering is
+         * started and immediately stopped before the link layer goes up, or if we get a link layer
+         * event without a prior call to startTethering (e.g. adb shell cmd wifi start-softap).
+         * @hide
+         */
+        public static final int REQUEST_TYPE_PLACEHOLDER = 3;
+
+        /**
+         * Type of request, used to keep track of whether the request was explicitly sent by
+         * startTethering, implicitly created by broadcasts, or via legacy tether().
+         * @hide
+         */
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef(prefix = "TYPE_", value = {
+                REQUEST_TYPE_EXPLICIT,
+                REQUEST_TYPE_IMPLICIT,
+                REQUEST_TYPE_LEGACY,
+                REQUEST_TYPE_PLACEHOLDER,
+        })
+        public @interface RequestType {}
+
         /** A configuration set for TetheringRequest. */
         private final TetheringRequestParcel mRequestParcel;
 
@@ -761,10 +894,12 @@
         }
 
         /** Builder used to create TetheringRequest. */
+        @SuppressLint({"UnflaggedApi", "StaticFinalBuilder"})
         public static class Builder {
             private final TetheringRequestParcel mBuilderParcel;
 
             /** Default constructor of Builder. */
+            @SuppressLint("UnflaggedApi")
             public Builder(@TetheringType final int type) {
                 mBuilderParcel = new TetheringRequestParcel();
                 mBuilderParcel.tetheringType = type;
@@ -775,6 +910,8 @@
                 mBuilderParcel.connectivityScope = getDefaultConnectivityScope(type);
                 mBuilderParcel.uid = Process.INVALID_UID;
                 mBuilderParcel.softApConfig = null;
+                mBuilderParcel.interfaceName = null;
+                mBuilderParcel.requestType = REQUEST_TYPE_EXPLICIT;
             }
 
             /**
@@ -785,7 +922,9 @@
              *
              * @param localIPv4Address The preferred local IPv4 link address to use.
              * @param clientAddress The static client address.
+             * @hide
              */
+            @SystemApi
             @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
             @NonNull
             public Builder setStaticIpv4Addresses(@NonNull final LinkAddress localIPv4Address,
@@ -801,7 +940,11 @@
                 return this;
             }
 
-            /** Start tethering without entitlement checks. */
+            /**
+             * Start tethering without entitlement checks.
+             * @hide
+             */
+            @SystemApi
             @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
             @NonNull
             public Builder setExemptFromEntitlementCheck(boolean exempt) {
@@ -812,7 +955,9 @@
             /**
              * If an entitlement check is needed, sets whether to show the entitlement UI or to
              * perform a silent entitlement check. By default, the entitlement UI is shown.
+             * @hide
              */
+            @SystemApi
             @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
             @NonNull
             public Builder setShouldShowEntitlementUi(boolean showUi) {
@@ -821,8 +966,39 @@
             }
 
             /**
-             * Sets the connectivity scope to be provided by this tethering downstream.
+             * Sets the name of the interface. Currently supported only for
+             * - {@link #TETHERING_VIRTUAL}.
+             * - {@link #TETHERING_WIFI} (for Local-only Hotspot)
+             * - {@link #TETHERING_WIFI_P2P}
+             * @hide
              */
+            @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
+            @RequiresPermission(anyOf = {
+                    android.Manifest.permission.NETWORK_SETTINGS,
+                    android.Manifest.permission.NETWORK_STACK,
+                    NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            })
+            @NonNull
+            @SystemApi(client = MODULE_LIBRARIES)
+            public Builder setInterfaceName(@Nullable final String interfaceName) {
+                switch (mBuilderParcel.tetheringType) {
+                    case TETHERING_VIRTUAL:
+                    case TETHERING_WIFI_P2P:
+                    case TETHERING_WIFI:
+                        break;
+                    default:
+                        throw new IllegalArgumentException("Interface name cannot be set for"
+                                + " tethering type " + interfaceName);
+                }
+                mBuilderParcel.interfaceName = interfaceName;
+                return this;
+            }
+
+            /**
+             * Sets the connectivity scope to be provided by this tethering downstream.
+             * @hide
+             */
+            @SystemApi
             @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
             @NonNull
             public Builder setConnectivityScope(@ConnectivityScope int scope) {
@@ -842,6 +1018,8 @@
              * If TETHERING_WIFI is already enabled and a new request is made with a different
              * SoftApConfiguration, the request will be accepted if the device can support an
              * additional tethering Wi-Fi AP interface. Otherwise, the request will be rejected.
+             * </p>
+             * Non-system callers using TETHERING_WIFI must specify a SoftApConfiguration.
              *
              * @param softApConfig SoftApConfiguration to use.
              * @throws IllegalArgumentException if the tethering type isn't TETHERING_WIFI.
@@ -860,6 +1038,7 @@
 
             /** Build {@link TetheringRequest} with the currently set configuration. */
             @NonNull
+            @SuppressLint("UnflaggedApi")
             public TetheringRequest build() {
                 return new TetheringRequest(mBuilderParcel);
             }
@@ -868,7 +1047,9 @@
         /**
          * Get the local IPv4 address, if one was configured with
          * {@link Builder#setStaticIpv4Addresses}.
+         * @hide
          */
+        @SystemApi
         @Nullable
         public LinkAddress getLocalIpv4Address() {
             return mRequestParcel.localIPv4Address;
@@ -877,35 +1058,64 @@
         /**
          * Get the static IPv4 address of the client, if one was configured with
          * {@link Builder#setStaticIpv4Addresses}.
+         * @hide
          */
+        @SystemApi
         @Nullable
         public LinkAddress getClientStaticIpv4Address() {
             return mRequestParcel.staticClientAddress;
         }
 
-        /** Get tethering type. */
+        /**
+         * Get tethering type.
+         * @hide
+         */
+        @SystemApi
         @TetheringType
         public int getTetheringType() {
             return mRequestParcel.tetheringType;
         }
 
-        /** Get connectivity type */
+        /**
+         * Get connectivity type
+         * @hide
+         */
+        @SystemApi
         @ConnectivityScope
         public int getConnectivityScope() {
             return mRequestParcel.connectivityScope;
         }
 
-        /** Check if exempt from entitlement check. */
+        /**
+         * Check if exempt from entitlement check.
+         * @hide
+         */
+        @SystemApi
         public boolean isExemptFromEntitlementCheck() {
             return mRequestParcel.exemptFromEntitlementCheck;
         }
 
-        /** Check if show entitlement ui.  */
+        /**
+         * Check if show entitlement ui.
+         * @hide
+         */
+        @SystemApi
         public boolean getShouldShowEntitlementUi() {
             return mRequestParcel.showProvisioningUi;
         }
 
         /**
+         * Get interface name.
+         * @hide
+         */
+        @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
+        @Nullable
+        @SystemApi(client = MODULE_LIBRARIES)
+        public String getInterfaceName() {
+            return mRequestParcel.interfaceName;
+        }
+
+        /**
          * Check whether the two addresses are ipv4 and in the same prefix.
          * @hide
          */
@@ -996,10 +1206,29 @@
             return mRequestParcel;
         }
 
-        /** String of TetheringRequest detail. */
+        /**
+         * Get the type of the request.
+         * @hide
+         */
+        public @RequestType int getRequestType() {
+            return mRequestParcel.requestType;
+        }
+
+        /**
+         * String of TetheringRequest detail.
+         * @hide
+         */
+        @SystemApi
         public String toString() {
             StringJoiner sj = new StringJoiner(", ", "TetheringRequest[ ", " ]");
             sj.add(typeToString(mRequestParcel.tetheringType));
+            if (mRequestParcel.requestType == REQUEST_TYPE_IMPLICIT) {
+                sj.add("IMPLICIT");
+            } else if (mRequestParcel.requestType == REQUEST_TYPE_LEGACY) {
+                sj.add("LEGACY");
+            } else if (mRequestParcel.requestType == REQUEST_TYPE_PLACEHOLDER) {
+                sj.add("PLACEHOLDER");
+            }
             if (mRequestParcel.localIPv4Address != null) {
                 sj.add("localIpv4Address=" + mRequestParcel.localIPv4Address);
             }
@@ -1022,43 +1251,67 @@
             if (mRequestParcel.packageName != null) {
                 sj.add("packageName=" + mRequestParcel.packageName);
             }
+            if (mRequestParcel.interfaceName != null) {
+                sj.add("interfaceName=" + mRequestParcel.interfaceName);
+            }
             return sj.toString();
         }
 
+        /**
+         * @hide
+         */
+        @SystemApi
         @Override
         public boolean equals(Object obj) {
             if (this == obj) return true;
             if (!(obj instanceof TetheringRequest otherRequest)) return false;
+            if (!equalsIgnoreUidPackage(otherRequest)) return false;
             TetheringRequestParcel parcel = getParcel();
             TetheringRequestParcel otherParcel = otherRequest.getParcel();
-            return parcel.tetheringType == otherParcel.tetheringType
+            return parcel.uid == otherParcel.uid
+                    && Objects.equals(parcel.packageName, otherParcel.packageName);
+        }
+
+        /**
+         * @hide
+         */
+        public boolean equalsIgnoreUidPackage(TetheringRequest otherRequest) {
+            TetheringRequestParcel parcel = getParcel();
+            TetheringRequestParcel otherParcel = otherRequest.getParcel();
+            return parcel.requestType == otherParcel.requestType
+                    && parcel.tetheringType == otherParcel.tetheringType
                     && Objects.equals(parcel.localIPv4Address, otherParcel.localIPv4Address)
                     && Objects.equals(parcel.staticClientAddress, otherParcel.staticClientAddress)
                     && parcel.exemptFromEntitlementCheck == otherParcel.exemptFromEntitlementCheck
                     && parcel.showProvisioningUi == otherParcel.showProvisioningUi
                     && parcel.connectivityScope == otherParcel.connectivityScope
                     && Objects.equals(parcel.softApConfig, otherParcel.softApConfig)
-                    && parcel.uid == otherParcel.uid
-                    && Objects.equals(parcel.packageName, otherParcel.packageName);
+                    && Objects.equals(parcel.interfaceName, otherParcel.interfaceName);
         }
 
+        /**
+         * @hide
+         */
+        @SystemApi
         @Override
         public int hashCode() {
             TetheringRequestParcel parcel = getParcel();
             return Objects.hash(parcel.tetheringType, parcel.localIPv4Address,
                     parcel.staticClientAddress, parcel.exemptFromEntitlementCheck,
                     parcel.showProvisioningUi, parcel.connectivityScope, parcel.softApConfig,
-                    parcel.uid, parcel.packageName);
+                    parcel.uid, parcel.packageName, parcel.interfaceName);
         }
     }
 
     /**
      * Callback for use with {@link #startTethering} to find out whether tethering succeeded.
      */
+    @SuppressLint("UnflaggedApi")
     public interface StartTetheringCallback {
         /**
          * Called when tethering has been successfully started.
          */
+        @SuppressLint("UnflaggedApi")
         default void onTetheringStarted() {}
 
         /**
@@ -1066,26 +1319,40 @@
          *
          * @param error The error that caused the failure.
          */
+        @SuppressLint("UnflaggedApi")
         default void onTetheringFailed(@StartTetheringError final int error) {}
     }
 
     /**
+     * Callback for use with {@link #stopTethering} to find out whether stop tethering succeeded.
+     */
+    @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
+    public interface StopTetheringCallback {
+        /**
+         * Called when tethering has been successfully stopped.
+         */
+        default void onStopTetheringSucceeded() {}
+
+        /**
+         * Called when starting tethering failed.
+         *
+         * @param error The error that caused the failure.
+         */
+        default void onStopTetheringFailed(@StopTetheringError final int error) {}
+    }
+
+    /**
      * Starts tethering and runs tether provisioning for the given type if needed. If provisioning
      * fails, stopTethering will be called automatically.
      *
-     * <p>Without {@link android.Manifest.permission.TETHER_PRIVILEGED} permission, the call will
-     * fail if a tethering entitlement check is required.
-     *
      * @param request a {@link TetheringRequest} which can specify the preferred configuration.
      * @param executor {@link Executor} to specify the thread upon which the callback of
      *         TetheringRequest will be invoked.
      * @param callback A callback that will be called to indicate the success status of the
      *                 tethering start request.
      */
-    @RequiresPermission(anyOf = {
-            android.Manifest.permission.TETHER_PRIVILEGED,
-            android.Manifest.permission.WRITE_SETTINGS
-    })
+    @RequiresPermission(value = android.Manifest.permission.TETHER_PRIVILEGED, conditional = true)
+    @SuppressLint("UnflaggedApi")
     public void startTethering(@NonNull final TetheringRequest request,
             @NonNull final Executor executor, @NonNull final StartTetheringCallback callback) {
         final String callerPkg = mContext.getOpPackageName();
@@ -1111,18 +1378,12 @@
      * Starts tethering and runs tether provisioning for the given type if needed. If provisioning
      * fails, stopTethering will be called automatically.
      *
-     * <p>Without {@link android.Manifest.permission.TETHER_PRIVILEGED} permission, the call will
-     * fail if a tethering entitlement check is required.
-     *
      * @param type The tethering type, on of the {@code TetheringManager#TETHERING_*} constants.
      * @param executor {@link Executor} to specify the thread upon which the callback of
      *         TetheringRequest will be invoked.
      * @hide
      */
-    @RequiresPermission(anyOf = {
-            android.Manifest.permission.TETHER_PRIVILEGED,
-            android.Manifest.permission.WRITE_SETTINGS
-    })
+    @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
     @SystemApi(client = MODULE_LIBRARIES)
     public void startTethering(int type, @NonNull final Executor executor,
             @NonNull final StartTetheringCallback callback) {
@@ -1133,13 +1394,10 @@
      * Stops tethering for the given type. Also cancels any provisioning rechecks for that type if
      * applicable.
      *
-     * <p>Without {@link android.Manifest.permission.TETHER_PRIVILEGED} permission, the call will
-     * fail if a tethering entitlement check is required.
+     * @hide
      */
-    @RequiresPermission(anyOf = {
-            android.Manifest.permission.TETHER_PRIVILEGED,
-            android.Manifest.permission.WRITE_SETTINGS
-    })
+    @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
+    @SystemApi
     public void stopTethering(@TetheringType final int type) {
         final String callerPkg = mContext.getOpPackageName();
         Log.i(TAG, "stopTethering caller:" + callerPkg);
@@ -1157,9 +1415,40 @@
     }
 
     /**
+     * Stops tethering for the given request. Operation will fail with
+     * {@link #TETHER_ERROR_UNKNOWN_REQUEST} if there is no request that matches it.
+     */
+    @RequiresPermission(value = android.Manifest.permission.TETHER_PRIVILEGED, conditional = true)
+    @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
+    public void stopTethering(@NonNull TetheringRequest request,
+            @NonNull final Executor executor, @NonNull final StopTetheringCallback callback) {
+        Objects.requireNonNull(request);
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
+
+        final String callerPkg = mContext.getOpPackageName();
+        Log.i(TAG, "stopTethering: request=" + request + ", caller=" + callerPkg);
+        getConnector(c -> c.stopTetheringRequest(request, callerPkg, getAttributionTag(),
+                new IIntResultListener.Stub() {
+                    @Override
+                    public void onResult(final int resultCode) {
+                        executor.execute(() -> {
+                            if (resultCode == TETHER_ERROR_NO_ERROR) {
+                                callback.onStopTetheringSucceeded();
+                            } else {
+                                callback.onStopTetheringFailed(resultCode);
+                            }
+                        });
+                    }
+                }));
+    }
+
+    /**
      * Callback for use with {@link #getLatestTetheringEntitlementResult} to find out whether
      * entitlement succeeded.
+     * @hide
      */
+    @SystemApi
     public interface OnTetheringEntitlementResultListener  {
         /**
          * Called to notify entitlement result.
@@ -1180,9 +1469,6 @@
      * {@link #TETHER_ERROR_ENTITLEMENT_UNKNOWN} will be returned. If {@code showEntitlementUi} is
      * true, entitlement will be run.
      *
-     * <p>Without {@link android.Manifest.permission.TETHER_PRIVILEGED} permission, the call will
-     * fail if a tethering entitlement check is required.
-     *
      * @param type the downstream type of tethering. Must be one of {@code #TETHERING_*} constants.
      * @param showEntitlementUi a boolean indicating whether to check result for the UI-based
      *         entitlement check or the silent entitlement check.
@@ -1190,11 +1476,10 @@
      * @param listener an {@link OnTetheringEntitlementResultListener} which will be called to
      *         notify the caller of the result of entitlement check. The listener may be called zero
      *         or one time.
+     * @hide
      */
-    @RequiresPermission(anyOf = {
-            android.Manifest.permission.TETHER_PRIVILEGED,
-            android.Manifest.permission.WRITE_SETTINGS
-    })
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
     public void requestLatestTetheringEntitlementResult(@TetheringType int type,
             boolean showEntitlementUi,
             @NonNull Executor executor,
@@ -1238,6 +1523,7 @@
      * Callback for use with {@link registerTetheringEventCallback} to find out tethering
      * upstream status.
      */
+    @SuppressLint("UnflaggedApi")
     public interface TetheringEventCallback {
         /**
          * Called when tethering supported status changed.
@@ -1249,7 +1535,9 @@
          * policy restrictions.
          *
          * @param supported whether any tethering type is supported.
+         * @hide
          */
+        @SystemApi
         default void onTetheringSupported(boolean supported) {}
 
         /**
@@ -1274,7 +1562,9 @@
          *
          * @param network the {@link Network} of tethering upstream. Null means tethering doesn't
          * have any upstream.
+         * @hide
          */
+        @SystemApi
         default void onUpstreamChanged(@Nullable Network network) {}
 
         /**
@@ -1301,7 +1591,9 @@
          * <p>This will be called immediately after the callback is registered, and may be called
          * multiple times later upon changes.
          * @param interfaces The list of tetherable interface names.
+         * @hide
          */
+        @SystemApi
         default void onTetherableInterfacesChanged(@NonNull List<String> interfaces) {}
 
         /**
@@ -1311,7 +1603,9 @@
          * <p>This will be called immediately after the callback is registered, and may be called
          * multiple times later upon changes.
          * @param interfaces The set of TetheringInterface of currently tetherable interface.
+         * @hide
          */
+        @SystemApi
         default void onTetherableInterfacesChanged(@NonNull Set<TetheringInterface> interfaces) {
             // By default, the new callback calls the old callback, so apps
             // implementing the old callback just work.
@@ -1324,7 +1618,9 @@
          * <p>This will be called immediately after the callback is registered, and may be called
          * multiple times later upon changes.
          * @param interfaces The lit of 0 or more String of currently tethered interface names.
+         * @hide
          */
+        @SystemApi
         default void onTetheredInterfacesChanged(@NonNull List<String> interfaces) {}
 
         /**
@@ -1335,6 +1631,7 @@
          * @param interfaces The set of 0 or more TetheringInterface of currently tethered
          * interface.
          */
+        @SuppressLint("UnflaggedApi")
         default void onTetheredInterfacesChanged(@NonNull Set<TetheringInterface> interfaces) {
             // By default, the new callback calls the old callback, so apps
             // implementing the old callback just work.
@@ -1347,7 +1644,9 @@
          * <p>This will be called immediately after the callback is registered, and may be called
          * multiple times later upon changes.
          * @param interfaces The list of 0 or more String of active local-only interface names.
+         * @hide
          */
+        @SystemApi
         default void onLocalOnlyInterfacesChanged(@NonNull List<String> interfaces) {}
 
         /**
@@ -1357,7 +1656,9 @@
          * multiple times later upon changes.
          * @param interfaces The set of 0 or more TetheringInterface of active local-only
          * interface.
+         * @hide
          */
+        @SystemApi
         default void onLocalOnlyInterfacesChanged(@NonNull Set<TetheringInterface> interfaces) {
             // By default, the new callback calls the old callback, so apps
             // implementing the old callback just work.
@@ -1371,7 +1672,9 @@
          * on the interface is an error, and may be called multiple times later upon changes.
          * @param ifName Name of the interface.
          * @param error One of {@code TetheringManager#TETHER_ERROR_*}.
+         * @hide
          */
+        @SystemApi
         default void onError(@NonNull String ifName, @TetheringIfaceError int error) {}
 
         /**
@@ -1381,7 +1684,9 @@
          * on the interface is an error, and may be called multiple times later upon changes.
          * @param iface The interface that experienced the error.
          * @param error One of {@code TetheringManager#TETHER_ERROR_*}.
+         * @hide
          */
+        @SystemApi
         default void onError(@NonNull TetheringInterface iface, @TetheringIfaceError int error) {
             // By default, the new callback calls the old callback, so apps
             // implementing the old callback just work.
@@ -1398,7 +1703,9 @@
          * clients may still be reported by this callback after disconnection as the system cannot
          * determine if they are still connected.
          * @param clients The new set of tethered clients; the collection is not ordered.
+         * @hide
          */
+        @SystemApi
         default void onClientsChanged(@NonNull Collection<TetheredClient> clients) {}
 
         /**
@@ -1406,7 +1713,9 @@
          *
          * <p>This will be called immediately after the callback is registered.
          * @param status The offload status.
+         * @hide
          */
+        @SystemApi
         default void onOffloadStatusChanged(@TetherOffloadStatus int status) {}
     }
 
@@ -1500,6 +1809,7 @@
      * @param callback the callback to be called when tethering has change events.
      */
     @RequiresPermission(Manifest.permission.ACCESS_NETWORK_STATE)
+    @SuppressLint("UnflaggedApi")
     public void registerTetheringEventCallback(@NonNull Executor executor,
             @NonNull TetheringEventCallback callback) {
         Objects.requireNonNull(executor);
@@ -1658,6 +1968,7 @@
             Manifest.permission.TETHER_PRIVILEGED,
             Manifest.permission.ACCESS_NETWORK_STATE
     })
+    @SuppressLint("UnflaggedApi")
     public void unregisterTetheringEventCallback(@NonNull final TetheringEventCallback callback) {
         Objects.requireNonNull(callback);
 
@@ -1848,14 +2159,10 @@
 
     /**
      * Stop all active tethering.
-     *
-     * <p>Without {@link android.Manifest.permission.TETHER_PRIVILEGED} permission, the call will
-     * fail if a tethering entitlement check is required.
+     * @hide
      */
-    @RequiresPermission(anyOf = {
-            android.Manifest.permission.TETHER_PRIVILEGED,
-            android.Manifest.permission.WRITE_SETTINGS
-    })
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
     public void stopAllTethering() {
         final String callerPkg = mContext.getOpPackageName();
         Log.i(TAG, "stopAllTethering caller:" + callerPkg);
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl b/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
index 789d5bb..9863385 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
@@ -24,6 +24,7 @@
  * @hide
  */
 parcelable TetheringRequestParcel {
+    int requestType;
     int tetheringType;
     LinkAddress localIPv4Address;
     LinkAddress staticClientAddress;
@@ -33,4 +34,5 @@
     SoftApConfiguration softApConfig;
     int uid;
     String packageName;
+    String interfaceName;
 }
diff --git a/Tethering/proguard.flags b/Tethering/proguard.flags
index 47e2848..6d857b1 100644
--- a/Tethering/proguard.flags
+++ b/Tethering/proguard.flags
@@ -1,3 +1,6 @@
+# Keep JNI registered methods
+-keepclasseswithmembers,includedescriptorclasses class * { native <methods>; }
+
 # Keep class's integer static field for MessageUtils to parsing their name.
 -keepclassmembers class com.android.server.**,android.net.**,com.android.networkstack.** {
     static final % POLICY_*;
@@ -7,18 +10,6 @@
     static final % EVENT_*;
 }
 
--keep class com.android.networkstack.tethering.util.BpfMap {
-    native <methods>;
-}
-
--keep class com.android.networkstack.tethering.util.TcUtils {
-    native <methods>;
-}
-
--keep class com.android.networkstack.tethering.util.TetheringUtils {
-    native <methods>;
-}
-
 # Ensure runtime-visible field annotations are kept when using R8 full mode.
 -keepattributes RuntimeVisibleAnnotations,AnnotationDefault
 -keep interface com.android.networkstack.tethering.util.Struct$Field {
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index d6f4572..609d759 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -16,6 +16,7 @@
 
 package android.net.ip;
 
+import static android.net.INetd.LOCAL_NET_ID;
 import static android.net.RouteInfo.RTN_UNICAST;
 import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
 import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
@@ -173,10 +174,10 @@
         /**
          * Request Tethering change.
          *
-         * @param request the TetheringRequest this IpServer was enabled with.
+         * @param tetheringType the downstream type of this IpServer.
          * @param enabled enable or disable tethering.
          */
-        public void requestEnableTethering(TetheringRequest request, boolean enabled) { }
+        public void requestEnableTethering(int tetheringType, boolean enabled) { }
     }
 
     /** Capture IpServer dependencies, for injection. */
@@ -428,9 +429,11 @@
         return Collections.unmodifiableList(mDhcpLeases);
     }
 
-    /** Enable this IpServer. IpServer state machine will be tethered or localHotspot state. */
-    public void enable(final int requestedState, final TetheringRequest request) {
-        sendMessage(CMD_TETHER_REQUESTED, requestedState, 0, request);
+    /**
+     * Enable this IpServer. IpServer state machine will be tethered or localHotspot state based on
+     * the connectivity scope of the TetheringRequest. */
+    public void enable(@NonNull final TetheringRequest request) {
+        sendMessage(CMD_TETHER_REQUESTED, 0, 0, request);
     }
 
     /** Stop this IpServer. After this is called this IpServer should not be used any more. */
@@ -906,7 +909,7 @@
             ArraySet<IpPrefix> deprecatedPrefixes, ArraySet<IpPrefix> newPrefixes) {
         // [1] Remove the routes that are deprecated.
         if (!deprecatedPrefixes.isEmpty()) {
-            removeRoutesFromNetworkAndLinkProperties(INetd.LOCAL_NET_ID,
+            removeRoutesFromNetworkAndLinkProperties(LOCAL_NET_ID,
                     getLocalRoutesFor(mIfaceName, deprecatedPrefixes));
         }
 
@@ -918,7 +921,7 @@
             }
 
             if (!addedPrefixes.isEmpty()) {
-                addRoutesToNetworkAndLinkProperties(INetd.LOCAL_NET_ID,
+                addRoutesToNetworkAndLinkProperties(LOCAL_NET_ID,
                         getLocalRoutesFor(mIfaceName, addedPrefixes));
             }
         }
@@ -1025,11 +1028,11 @@
         mLinkProperties.setInterfaceName(mIfaceName);
     }
 
-    private void maybeConfigureStaticIp(final TetheringRequest request) {
+    private void maybeConfigureStaticIp(@NonNull final TetheringRequest request) {
         // Ignore static address configuration if they are invalid or null. In theory, static
         // addresses should not be invalid here because TetheringManager do not allow caller to
         // specify invalid static address configuration.
-        if (request == null || request.getLocalIpv4Address() == null
+        if (request.getLocalIpv4Address() == null
                 || request.getClientStaticIpv4Address() == null || !checkStaticAddressConfiguration(
                 request.getLocalIpv4Address(), request.getClientStaticIpv4Address())) {
             return;
@@ -1052,13 +1055,13 @@
                 case CMD_TETHER_REQUESTED:
                     mLastError = TETHER_ERROR_NO_ERROR;
                     mTetheringRequest = (TetheringRequest) message.obj;
-                    switch (message.arg1) {
-                        case STATE_LOCAL_ONLY:
-                            maybeConfigureStaticIp((TetheringRequest) message.obj);
+                    switch (mTetheringRequest.getConnectivityScope()) {
+                        case CONNECTIVITY_SCOPE_LOCAL:
+                            maybeConfigureStaticIp(mTetheringRequest);
                             transitionTo(mLocalHotspotState);
                             break;
-                        case STATE_TETHERED:
-                            maybeConfigureStaticIp((TetheringRequest) message.obj);
+                        case CONNECTIVITY_SCOPE_GLOBAL:
+                            maybeConfigureStaticIp(mTetheringRequest);
                             transitionTo(mTetheredState);
                             break;
                         default:
@@ -1123,7 +1126,7 @@
             }
 
             try {
-                NetdUtils.tetherInterface(mNetd, INetd.LOCAL_NET_ID, mIfaceName,
+                NetdUtils.tetherInterface(mNetd, LOCAL_NET_ID, mIfaceName,
                         asIpPrefix(mIpv4Address));
             } catch (RemoteException | ServiceSpecificException | IllegalStateException e) {
                 mLog.e("Error Tethering", e);
@@ -1146,7 +1149,7 @@
             stopIPv6();
 
             try {
-                NetdUtils.untetherInterface(mNetd, mIfaceName);
+                NetdUtils.untetherInterface(mNetd, LOCAL_NET_ID, mIfaceName);
             } catch (RemoteException | ServiceSpecificException e) {
                 mLastError = TETHER_ERROR_UNTETHER_IFACE_ERROR;
                 mLog.e("Failed to untether interface: " + e);
@@ -1188,8 +1191,8 @@
                     handleNewPrefixRequest((IpPrefix) message.obj);
                     break;
                 case CMD_NOTIFY_PREFIX_CONFLICT:
-                    mLog.i("restart tethering: " + mIfaceName);
-                    mCallback.requestEnableTethering(mTetheringRequest, false /* enabled */);
+                    mLog.i("restart tethering: " + mInterfaceType);
+                    mCallback.requestEnableTethering(mInterfaceType, false /* enabled */);
                     transitionTo(mWaitingForRestartState);
                     break;
                 case CMD_SERVICE_FAILED_TO_START:
@@ -1224,12 +1227,12 @@
             }
 
             // Remove deprecated routes from downstream network.
-            removeRoutesFromNetworkAndLinkProperties(INetd.LOCAL_NET_ID,
+            removeRoutesFromNetworkAndLinkProperties(LOCAL_NET_ID,
                     List.of(getDirectConnectedRoute(deprecatedLinkAddress)));
             mLinkProperties.removeLinkAddress(deprecatedLinkAddress);
 
             // Add new routes to downstream network.
-            addRoutesToNetworkAndLinkProperties(INetd.LOCAL_NET_ID,
+            addRoutesToNetworkAndLinkProperties(LOCAL_NET_ID,
                     List.of(getDirectConnectedRoute(mIpv4Address)));
             mLinkProperties.addLinkAddress(mIpv4Address);
 
@@ -1473,12 +1476,12 @@
                 case CMD_TETHER_UNREQUESTED:
                     transitionTo(mInitialState);
                     mLog.i("Untethered (unrequested) and restarting " + mIfaceName);
-                    mCallback.requestEnableTethering(mTetheringRequest, true /* enabled */);
+                    mCallback.requestEnableTethering(mInterfaceType, true /* enabled */);
                     break;
                 case CMD_INTERFACE_DOWN:
                     transitionTo(mUnavailableState);
                     mLog.i("Untethered (interface down) and restarting " + mIfaceName);
-                    mCallback.requestEnableTethering(mTetheringRequest, true /* enabled */);
+                    mCallback.requestEnableTethering(mInterfaceType, true /* enabled */);
                     break;
                 default:
                     return false;
diff --git a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
index fb16226..900b505 100644
--- a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
+++ b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
@@ -62,6 +62,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.FrameworkConnectivityStatsLog;
 import com.android.net.module.util.SharedLog;
 
 import java.io.PrintWriter;
@@ -159,7 +160,22 @@
                 // operates on the context user.
                 final int currentUserId = getCurrentUser();
                 final UserHandle currentUser = UserHandle.of(currentUserId);
-                final Context userContext = mContext.createContextAsUser(currentUser, 0);
+                final Context userContext;
+                try {
+                    // There is no safe way to invoke this method since tethering package
+                    // might not be installed for a certain user on the OEM devices,
+                    // refer to b/382628161.
+                    userContext = mContext.createContextAsUser(currentUser, 0);
+                } catch (IllegalStateException e) {
+                    FrameworkConnectivityStatsLog.write(
+                            FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+                            FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_ENTITLEMENT_CREATE_CONTEXT_AS_USER_THROWS
+                    );
+                    // Fallback to startActivity if createContextAsUser failed.
+                    mLog.e("createContextAsUser failed, fallback to startActivity", e);
+                    mContext.startActivity(intent);
+                    return intent;
+                }
                 final UserManager userManager = userContext.getSystemService(UserManager.class);
 
                 if (userManager.isAdminUser()) {
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index f33ef37..b50831d 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -28,6 +28,7 @@
 import static android.net.ConnectivityManager.EXTRA_NETWORK_INFO;
 import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
 import static android.net.TetheringManager.ACTION_TETHER_STATE_CHANGED;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
 import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
 import static android.net.TetheringManager.EXTRA_ACTIVE_LOCAL_ONLY;
 import static android.net.TetheringManager.EXTRA_ACTIVE_TETHER;
@@ -42,15 +43,18 @@
 import static android.net.TetheringManager.TETHERING_WIFI;
 import static android.net.TetheringManager.TETHERING_WIFI_P2P;
 import static android.net.TetheringManager.TETHERING_WIGIG;
+import static android.net.TetheringManager.TETHER_ERROR_BLUETOOTH_SERVICE_PENDING;
 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_UNAVAIL_IFACE;
 import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_IFACE;
+import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_REQUEST;
 import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_TYPE;
 import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_FAILED;
 import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STARTED;
 import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STOPPED;
+import static android.net.TetheringManager.TetheringRequest.REQUEST_TYPE_PLACEHOLDER;
 import static android.net.TetheringManager.toIfaces;
 import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_INTERFACE_NAME;
 import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_MODE;
@@ -67,6 +71,12 @@
 import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_FORCE_USB_FUNCTIONS;
 import static com.android.networkstack.tethering.TetheringNotificationUpdater.DOWNSTREAM_NONE;
 import static com.android.networkstack.tethering.UpstreamNetworkMonitor.isCellular;
+import static com.android.networkstack.tethering.metrics.TetheringStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED;
+import static com.android.networkstack.tethering.metrics.TetheringStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI;
+import static com.android.networkstack.tethering.metrics.TetheringStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_P2P;
+import static com.android.networkstack.tethering.metrics.TetheringStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_P2P_SUCCESS;
+import static com.android.networkstack.tethering.metrics.TetheringStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_SUCCESS;
+import static com.android.networkstack.tethering.metrics.TetheringStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_TETHER_WITH_PLACEHOLDER_REQUEST;
 import static com.android.networkstack.tethering.util.TetheringMessageBase.BASE_MAIN_SM;
 
 import android.app.usage.NetworkStatsManager;
@@ -105,6 +115,7 @@
 import android.net.wifi.p2p.WifiP2pInfo;
 import android.net.wifi.p2p.WifiP2pManager;
 import android.os.Binder;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Looper;
@@ -122,7 +133,6 @@
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.Log;
-import android.util.Pair;
 import android.util.SparseArray;
 
 import androidx.annotation.NonNull;
@@ -140,11 +150,13 @@
 import com.android.net.module.util.NetdUtils;
 import com.android.net.module.util.RoutingCoordinatorManager;
 import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.TerribleErrorLog;
 import com.android.networkstack.apishim.common.BluetoothPanShim;
 import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceCallbackShim;
 import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceRequestShim;
 import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
 import com.android.networkstack.tethering.metrics.TetheringMetrics;
+import com.android.networkstack.tethering.metrics.TetheringStatsLog;
 import com.android.networkstack.tethering.util.InterfaceSet;
 import com.android.networkstack.tethering.util.PrefixUtils;
 import com.android.networkstack.tethering.util.VersionedBroadcastListener;
@@ -232,7 +244,7 @@
     // Currently active tethering requests per tethering type. Only one of each type can be
     // requested at a time. After a tethering type is requested, the map keeps tethering parameters
     // to be used after the interface comes up asynchronously.
-    private final SparseArray<TetheringRequest> mActiveTetheringRequests =
+    private final SparseArray<TetheringRequest> mPendingTetheringRequests =
             new SparseArray<>();
 
     private final Context mContext;
@@ -280,7 +292,7 @@
     private SettingsObserver mSettingsObserver;
     private BluetoothPan mBluetoothPan;
     private PanServiceListener mBluetoothPanListener;
-    private ArrayList<Pair<Boolean, IIntResultListener>> mPendingPanRequests;
+    private final ArrayList<IIntResultListener> mPendingPanRequestListeners;
     // AIDL doesn't support Set<Integer>. Maintain a int bitmap here. When the bitmap is passed to
     // TetheringManager, TetheringManager would convert it to a set of Integer types.
     // mSupportedTypeBitmap should always be updated inside tethering internal thread but it may be
@@ -300,7 +312,7 @@
         // This is intended to ensrure that if something calls startTethering(bluetooth) just after
         // bluetooth is enabled. Before onServiceConnected is called, store the calls into this
         // list and handle them as soon as onServiceConnected is called.
-        mPendingPanRequests = new ArrayList<>();
+        mPendingPanRequestListeners = new ArrayList<>();
 
         mTetherStates = new ArrayMap<>();
         mConnectedClientsTracker = new ConnectedClientsTracker();
@@ -451,6 +463,10 @@
         return mSettingsObserver;
     }
 
+    boolean isTetheringWithSoftApConfigEnabled() {
+        return mDeps.isTetheringWithSoftApConfigEnabled();
+    }
+
     /**
      * Start to register callbacks.
      * Call this function when tethering is ready to handle callback events.
@@ -592,15 +608,42 @@
     // This method needs to exist because TETHERING_BLUETOOTH before Android T and TETHERING_WIGIG
     // can't use enableIpServing.
     private void processInterfaceStateChange(final String iface, boolean enabled) {
+        final int type = ifaceNameToType(iface);
         // Do not listen to USB interface state changes or USB interface add/removes. USB tethering
         // is driven only by USB_ACTION broadcasts.
-        final int type = ifaceNameToType(iface);
         if (type == TETHERING_USB || type == TETHERING_NCM) return;
 
+        // On T+, BLUETOOTH uses enableIpServing.
         if (type == TETHERING_BLUETOOTH && SdkLevel.isAtLeastT()) return;
 
+        // Cannot happen: on S+, tetherableWigigRegexps is always empty.
+        if (type == TETHERING_WIGIG && SdkLevel.isAtLeastS()) return;
+
+        // After V, disallow this legacy codepath from starting tethering of any type:
+        // everything must call ensureIpServerStarted directly.
+        //
+        // Don't touch the teardown path for now. It's more complicated because:
+        // - ensureIpServerStarted and ensureIpServerStopped act on different
+        //   tethering types.
+        // - Depending on the type, ensureIpServerStopped is either called twice (once
+        //   on interface down and once on interface removed) or just once (on
+        //   interface removed).
+        //
+        // Note that this only affects WIFI and WIFI_P2P. The other types are either
+        // ignored above, or ignored by ensureIpServerStarted. Note that even for WIFI
+        // and WIFI_P2P, this code should not ever run in normal use, because the
+        // hotspot and p2p code do not call tether(). It's possible that this could
+        // happen in the field due to unforeseen OEM modifications. If it does happen,
+        // a terrible error is logged in tether().
+        // TODO: fix the teardown path to stop depending on interface state notifications.
+        // These are not necessary since most/all link layers have their own teardown
+        // notifications, and can race with those notifications.
+        if (enabled && Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+            return;
+        }
+
         if (enabled) {
-            ensureIpServerStarted(iface);
+            ensureIpServerStartedForInterface(iface);
         } else {
             ensureIpServerStopped(iface);
         }
@@ -661,14 +704,14 @@
             final IIntResultListener listener) {
         mHandler.post(() -> {
             final int type = request.getTetheringType();
-            final TetheringRequest unfinishedRequest = mActiveTetheringRequests.get(type);
+            final TetheringRequest unfinishedRequest = mPendingTetheringRequests.get(type);
             // If tethering is already enabled with a different request,
             // disable before re-enabling.
-            if (unfinishedRequest != null && !unfinishedRequest.equals(request)) {
-                enableTetheringInternal(type, false /* disabled */, null);
+            if (unfinishedRequest != null && !unfinishedRequest.equalsIgnoreUidPackage(request)) {
+                enableTetheringInternal(false /* disabled */, unfinishedRequest, null);
                 mEntitlementMgr.stopProvisioningIfNeeded(type);
             }
-            mActiveTetheringRequests.put(type, request);
+            mPendingTetheringRequests.put(type, request);
 
             if (request.isExemptFromEntitlementCheck()) {
                 mEntitlementMgr.setExemptedDownstreamType(type);
@@ -676,7 +719,7 @@
                 mEntitlementMgr.startProvisioningIfNeeded(type,
                         request.getShouldShowEntitlementUi());
             }
-            enableTetheringInternal(type, true /* enabled */, listener);
+            enableTetheringInternal(true /* enabled */, request, listener);
             mTetheringMetrics.createBuilder(type, callerPkg);
         });
     }
@@ -686,10 +729,47 @@
             stopTetheringInternal(type);
         });
     }
-    void stopTetheringInternal(int type) {
-        mActiveTetheringRequests.remove(type);
 
-        enableTetheringInternal(type, false /* disabled */, null);
+    private boolean isTetheringTypePendingOrServing(final int type) {
+        for (int i = 0; i < mPendingTetheringRequests.size(); i++) {
+            if (mPendingTetheringRequests.valueAt(i).getTetheringType() == type) return true;
+        }
+        for (TetherState state : mTetherStates.values()) {
+            // TODO: isCurrentlyServing only starts returning true once the IpServer has processed
+            // the CMD_TETHER_REQUESTED. Ensure that we consider the request to be serving even when
+            // that has not happened yet.
+            if (state.isCurrentlyServing() && state.ipServer.interfaceType() == type) return true;
+        }
+        return false;
+    }
+
+    void stopTetheringRequest(@NonNull final TetheringRequest request,
+            @NonNull final IIntResultListener listener) {
+        if (!isTetheringWithSoftApConfigEnabled()) return;
+        mHandler.post(() -> {
+            final int type = request.getTetheringType();
+            if (isTetheringTypePendingOrServing(type)) {
+                stopTetheringInternal(type);
+                try {
+                    listener.onResult(TETHER_ERROR_NO_ERROR);
+                } catch (RemoteException ignored) { }
+                return;
+            }
+
+            // Request doesn't match any active requests, ignore.
+            try {
+                listener.onResult(TETHER_ERROR_UNKNOWN_REQUEST);
+            } catch (RemoteException ignored) { }
+        });
+    }
+
+    void stopTetheringInternal(int type) {
+        mPendingTetheringRequests.remove(type);
+
+        // Using a placeholder here is ok since none of the disable APIs use the request for
+        // anything. We simply need the tethering type to know which link layer to poke for removal.
+        // TODO: Remove the placeholder here and loop through each pending/serving request.
+        enableTetheringInternal(false /* disabled */, createPlaceholderRequest(type), null);
         mEntitlementMgr.stopProvisioningIfNeeded(type);
     }
 
@@ -697,9 +777,10 @@
      * Enables or disables tethering for the given type. If provisioning is required, it will
      * schedule provisioning rechecks for the specified interface.
      */
-    private void enableTetheringInternal(int type, boolean enable,
+    private void enableTetheringInternal(boolean enable, @NonNull final TetheringRequest request,
             final IIntResultListener listener) {
-        int result = TETHER_ERROR_NO_ERROR;
+        final int type = request.getTetheringType();
+        final int result;
         switch (type) {
             case TETHERING_WIFI:
                 result = setWifiTethering(enable);
@@ -708,7 +789,7 @@
                 result = setUsbTethering(enable);
                 break;
             case TETHERING_BLUETOOTH:
-                setBluetoothTethering(enable, listener);
+                result = setBluetoothTethering(enable, listener);
                 break;
             case TETHERING_NCM:
                 result = setNcmTethering(enable);
@@ -717,17 +798,17 @@
                 result = setEthernetTethering(enable);
                 break;
             case TETHERING_VIRTUAL:
-                result = setVirtualMachineTethering(enable);
+                result = setVirtualMachineTethering(enable, request);
                 break;
             default:
                 Log.w(TAG, "Invalid tether type.");
                 result = TETHER_ERROR_UNKNOWN_TYPE;
         }
 
-        // The result of Bluetooth tethering will be sent by #setBluetoothTethering.
-        if (type != TETHERING_BLUETOOTH) {
-            sendTetherResult(listener, result, type);
-        }
+        // The result of Bluetooth tethering will be sent after the pan service connects.
+        if (result == TETHER_ERROR_BLUETOOTH_SERVICE_PENDING) return;
+
+        sendTetherResult(listener, result, type);
     }
 
     private void sendTetherResult(final IIntResultListener listener, final int result,
@@ -741,7 +822,7 @@
         // If changing tethering fail, remove corresponding request
         // no matter who trigger the start/stop.
         if (result != TETHER_ERROR_NO_ERROR) {
-            mActiveTetheringRequests.remove(type);
+            mPendingTetheringRequests.remove(type);
             mTetheringMetrics.updateErrorCode(type, result);
             mTetheringMetrics.sendReport(type);
         }
@@ -766,13 +847,12 @@
         return TETHER_ERROR_INTERNAL_ERROR;
     }
 
-    private void setBluetoothTethering(final boolean enable, final IIntResultListener listener) {
+    private int setBluetoothTethering(final boolean enable, final IIntResultListener listener) {
         final BluetoothAdapter adapter = mDeps.getBluetoothAdapter();
         if (adapter == null || !adapter.isEnabled()) {
             Log.w(TAG, "Tried to enable bluetooth tethering with null or disabled adapter. null: "
                     + (adapter == null));
-            sendTetherResult(listener, TETHER_ERROR_SERVICE_UNAVAIL, TETHERING_BLUETOOTH);
-            return;
+            return TETHER_ERROR_SERVICE_UNAVAIL;
         }
 
         if (mBluetoothPanListener != null && mBluetoothPanListener.isConnected()) {
@@ -780,16 +860,21 @@
             // When bluetooth tethering is enabled, any time a PAN client pairs with this
             // host, bluetooth will bring up a bt-pan interface and notify tethering to
             // enable IP serving.
-            setBluetoothTetheringSettings(mBluetoothPan, enable, listener);
-            return;
+            return setBluetoothTetheringSettings(mBluetoothPan, enable);
         }
 
-        // The reference of IIntResultListener should only exist when application want to start
-        // tethering but tethering is not bound to pan service yet. Even if the calling process
-        // dies, the referenice of IIntResultListener would still keep in mPendingPanRequests. Once
-        // tethering bound to pan service (onServiceConnected) or bluetooth just crash
-        // (onServiceDisconnected), all the references from mPendingPanRequests would be cleared.
-        mPendingPanRequests.add(new Pair(enable, listener));
+        if (!enable) {
+            // The service is not connected. If disabling tethering, there's no point starting
+            // the service just to stop tethering since tethering is not started. Just remove
+            // any pending requests to enable tethering, and notify them that they have failed.
+            for (IIntResultListener pendingListener : mPendingPanRequestListeners) {
+                sendTetherResult(pendingListener, TETHER_ERROR_SERVICE_UNAVAIL,
+                        TETHERING_BLUETOOTH);
+            }
+            mPendingPanRequestListeners.clear();
+            return TETHER_ERROR_NO_ERROR;
+        }
+        mPendingPanRequestListeners.add(listener);
 
         // Bluetooth tethering is not a popular feature. To avoid bind to bluetooth pan service all
         // the time but user never use bluetooth tethering. mBluetoothPanListener is created first
@@ -799,6 +884,7 @@
             mBluetoothPanListener = new PanServiceListener();
             adapter.getProfileProxy(mContext, mBluetoothPanListener, BluetoothProfile.PAN);
         }
+        return TETHER_ERROR_BLUETOOTH_SERVICE_PENDING;
     }
 
     private class PanServiceListener implements ServiceListener {
@@ -815,10 +901,12 @@
                 mBluetoothPan = (BluetoothPan) proxy;
                 mIsConnected = true;
 
-                for (Pair<Boolean, IIntResultListener> request : mPendingPanRequests) {
-                    setBluetoothTetheringSettings(mBluetoothPan, request.first, request.second);
+                for (IIntResultListener pendingListener : mPendingPanRequestListeners) {
+                    final int result = setBluetoothTetheringSettings(mBluetoothPan,
+                            true /* enable */);
+                    sendTetherResult(pendingListener, result, TETHERING_BLUETOOTH);
                 }
-                mPendingPanRequests.clear();
+                mPendingPanRequestListeners.clear();
             });
         }
 
@@ -829,11 +917,11 @@
                 // reachable before next onServiceConnected.
                 mIsConnected = false;
 
-                for (Pair<Boolean, IIntResultListener> request : mPendingPanRequests) {
-                    sendTetherResult(request.second, TETHER_ERROR_SERVICE_UNAVAIL,
+                for (IIntResultListener pendingListener : mPendingPanRequestListeners) {
+                    sendTetherResult(pendingListener, TETHER_ERROR_SERVICE_UNAVAIL,
                             TETHERING_BLUETOOTH);
                 }
-                mPendingPanRequests.clear();
+                mPendingPanRequestListeners.clear();
                 mBluetoothIfaceRequest = null;
                 mBluetoothCallback = null;
                 maybeDisableBluetoothIpServing();
@@ -845,8 +933,8 @@
         }
     }
 
-    private void setBluetoothTetheringSettings(@NonNull final BluetoothPan bluetoothPan,
-            final boolean enable, final IIntResultListener listener) {
+    private int setBluetoothTetheringSettings(@NonNull final BluetoothPan bluetoothPan,
+            final boolean enable) {
         if (SdkLevel.isAtLeastT()) {
             changeBluetoothTetheringSettings(bluetoothPan, enable);
         } else {
@@ -855,9 +943,8 @@
 
         // Enabling bluetooth tethering settings can silently fail. Send internal error if the
         // result is not expected.
-        final int result = bluetoothPan.isTetheringOn() == enable
+        return bluetoothPan.isTetheringOn() == enable
                 ? TETHER_ERROR_NO_ERROR : TETHER_ERROR_INTERNAL_ERROR;
-        sendTetherResult(listener, result, TETHERING_BLUETOOTH);
     }
 
     private void changeBluetoothTetheringSettingsPreT(@NonNull final BluetoothPan bluetoothPan,
@@ -903,7 +990,9 @@
         public void onAvailable(String iface) {
             if (this != mBluetoothCallback) return;
 
-            enableIpServing(TETHERING_BLUETOOTH, iface, getRequestedState(TETHERING_BLUETOOTH));
+            final TetheringRequest request =
+                    getOrCreatePendingTetheringRequest(TETHERING_BLUETOOTH);
+            enableIpServing(request, iface);
             mConfiguredBluetoothIface = iface;
         }
 
@@ -958,7 +1047,9 @@
                 // Ethernet callback arrived after Ethernet tethering stopped. Ignore.
                 return;
             }
-            enableIpServing(TETHERING_ETHERNET, iface, getRequestedState(TETHERING_ETHERNET));
+
+            final TetheringRequest request = getOrCreatePendingTetheringRequest(TETHERING_ETHERNET);
+            enableIpServing(request, iface);
             mConfiguredEthernetIface = iface;
         }
 
@@ -972,14 +1063,16 @@
         }
     }
 
-    private int setVirtualMachineTethering(final boolean enable) {
-        // TODO(340377643): Use bridge ifname when it's introduced, not fixed TAP ifname.
+    private int setVirtualMachineTethering(final boolean enable,
+            @NonNull final TetheringRequest request) {
+        final String iface = request.getInterfaceName();
         if (enable) {
-            mConfiguredVirtualIface = "avf_tap_fixed";
-            enableIpServing(
-                    TETHERING_VIRTUAL,
-                    mConfiguredVirtualIface,
-                    getRequestedState(TETHERING_VIRTUAL));
+            if (TextUtils.isEmpty(iface)) {
+                mConfiguredVirtualIface = "avf_tap_fixed";
+            } else {
+                mConfiguredVirtualIface = iface;
+            }
+            enableIpServing(request, mConfiguredVirtualIface);
         } else if (mConfiguredVirtualIface != null) {
             ensureIpServerStopped(mConfiguredVirtualIface);
             mConfiguredVirtualIface = null;
@@ -987,15 +1080,132 @@
         return TETHER_ERROR_NO_ERROR;
     }
 
-    void tether(String iface, int requestedState, final IIntResultListener listener) {
-        mHandler.post(() -> {
-            try {
-                listener.onResult(tether(iface, requestedState));
-            } catch (RemoteException e) { }
-        });
+    /**
+     * Create a legacy tethering request for calls to the legacy tether() API, which doesn't take an
+     * explicit request. These are always CONNECTIVITY_SCOPE_GLOBAL, per historical behavior.
+     */
+    private TetheringRequest createLegacyGlobalScopeTetheringRequest(int type) {
+        final TetheringRequest request = new TetheringRequest.Builder(type).build();
+        request.getParcel().requestType = TetheringRequest.REQUEST_TYPE_LEGACY;
+        request.getParcel().connectivityScope = CONNECTIVITY_SCOPE_GLOBAL;
+        return request;
     }
 
-    private int tether(String iface, int requestedState) {
+    /**
+     * Create a local-only implicit tethering request. This is used for Wifi local-only hotspot and
+     * Wifi P2P, which start tethering based on the WIFI_(AP/P2P)_STATE_CHANGED broadcasts.
+     */
+    @NonNull
+    private TetheringRequest createImplicitLocalOnlyTetheringRequest(int type) {
+        final TetheringRequest request = new TetheringRequest.Builder(type).build();
+        request.getParcel().requestType = TetheringRequest.REQUEST_TYPE_IMPLICIT;
+        request.getParcel().connectivityScope = CONNECTIVITY_SCOPE_LOCAL;
+        return request;
+    }
+
+    /**
+     * Create a placeholder request. This is used in case we try to find a pending request but there
+     * is none (e.g. stopTethering removed a pending request), or for cases where we only have the
+     * tethering type (e.g. stopTethering(int)).
+     */
+    @NonNull
+    private TetheringRequest createPlaceholderRequest(int type) {
+        final TetheringRequest request = new TetheringRequest.Builder(type).build();
+        request.getParcel().requestType = TetheringRequest.REQUEST_TYPE_LEGACY;
+        request.getParcel().connectivityScope = CONNECTIVITY_SCOPE_GLOBAL;
+        return request;
+    }
+
+    /**
+     * Gets the TetheringRequest that #startTethering was called with but is waiting for the link
+     * layer event to indicate the interface is available to tether.
+     * Note: There are edge cases where the pending request is absent and we must temporarily
+     *       synthesize a placeholder request, such as if stopTethering was called before link layer
+     *       went up, or if the link layer goes up without us poking it (e.g. adb shell cmd wifi
+     *       start-softap). These placeholder requests only specify the tethering type and the
+     *       default connectivity scope.
+     */
+    @NonNull
+    private TetheringRequest getOrCreatePendingTetheringRequest(int type) {
+        TetheringRequest pending = mPendingTetheringRequests.get(type);
+        if (pending != null) return pending;
+
+        Log.w(TAG, "No pending TetheringRequest for type " + type + " found, creating a placeholder"
+                + " request");
+        return createPlaceholderRequest(type);
+    }
+
+    private void handleLegacyTether(String iface, final IIntResultListener listener) {
+        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+            // After V, the TetheringManager and ConnectivityManager tether and untether methods
+            // throw UnsupportedOperationException, so this cannot happen in normal use. Ensure
+            // that this code cannot run even if callers use raw binder calls or other
+            // unsupported methods.
+            return;
+        }
+
+        final int type = ifaceNameToType(iface);
+        if (type == TETHERING_INVALID) {
+            Log.e(TAG, "Ignoring call to legacy tether for unknown iface " + iface);
+            try {
+                listener.onResult(TETHER_ERROR_UNKNOWN_IFACE);
+            } catch (RemoteException e) { }
+        }
+
+        final TetheringRequest request = createLegacyGlobalScopeTetheringRequest(type);
+        int result = tetherInternal(request, iface);
+        switch (type) {
+            case TETHERING_WIFI:
+                TerribleErrorLog.logTerribleError(TetheringStatsLog::write,
+                        "Legacy tether API called on Wifi iface " + iface,
+                        CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+                        CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI);
+                if (result == TETHER_ERROR_NO_ERROR) {
+                    TerribleErrorLog.logTerribleError(TetheringStatsLog::write,
+                            "Legacy tether API succeeded on Wifi iface " + iface,
+                            CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+                            CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_SUCCESS);
+                }
+                break;
+            case TETHERING_WIFI_P2P:
+                TerribleErrorLog.logTerribleError(TetheringStatsLog::write,
+                        "Legacy tether API called on Wifi P2P iface " + iface,
+                        CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+                        CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_P2P);
+                if (result == TETHER_ERROR_NO_ERROR) {
+                    TerribleErrorLog.logTerribleError(TetheringStatsLog::write,
+                            "Legacy tether API succeeded on Wifi P2P iface " + iface,
+                            CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+                            CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_P2P_SUCCESS);
+                }
+                break;
+            default:
+                // Do nothing
+                break;
+        }
+        try {
+            listener.onResult(result);
+        } catch (RemoteException e) { }
+    }
+
+    /**
+     * Legacy tether API that starts tethering with CONNECTIVITY_SCOPE_GLOBAL on the given iface.
+     *
+     * This API relies on the IpServer having been started for the interface by
+     * processInterfaceStateChanged beforehand, which is only possible for
+     *     - WIGIG Pre-S
+     *     - BLUETOOTH Pre-T
+     *     - WIFI
+     *     - WIFI_P2P.
+     * Note that WIFI and WIFI_P2P already start tethering on their respective ifaces via
+     * WIFI_(AP/P2P_STATE_CHANGED broadcasts, which makes this API redundant for those types unless
+     * those broadcasts are disabled by OEM.
+     */
+    void legacyTether(String iface, final IIntResultListener listener) {
+        mHandler.post(() -> handleLegacyTether(iface, listener));
+    }
+
+    private int tetherInternal(@NonNull TetheringRequest request, String iface) {
         if (DBG) Log.d(TAG, "Tethering " + iface);
         TetherState tetherState = mTetherStates.get(iface);
         if (tetherState == null) {
@@ -1008,29 +1218,38 @@
             Log.e(TAG, "Tried to Tether an unavailable iface: " + iface + ", ignoring");
             return TETHER_ERROR_UNAVAIL_IFACE;
         }
-        // NOTE: If a CMD_TETHER_REQUESTED message is already in the TISM's queue but not yet
+        // NOTE: If a CMD_TETHER_REQUESTED message is already in the IpServer's queue but not yet
         // processed, this will be a no-op and it will not return an error.
         //
         // This code cannot race with untether() because they both run on the handler thread.
-        final int type = tetherState.ipServer.interfaceType();
-        final TetheringRequest request = mActiveTetheringRequests.get(type, null);
-        if (request != null) {
-            mActiveTetheringRequests.delete(type);
+        mPendingTetheringRequests.remove(request.getTetheringType());
+        tetherState.ipServer.enable(request);
+        if (request.getRequestType() == REQUEST_TYPE_PLACEHOLDER) {
+            TerribleErrorLog.logTerribleError(TetheringStatsLog::write,
+                    "Started tethering with placeholder request: " + request,
+                    CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+                    CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_TETHER_WITH_PLACEHOLDER_REQUEST);
         }
-        tetherState.ipServer.enable(requestedState, request);
         return TETHER_ERROR_NO_ERROR;
     }
 
-    void untether(String iface, final IIntResultListener listener) {
+    void legacyUntether(String iface, final IIntResultListener listener) {
+        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+            // After V, the TetheringManager and ConnectivityManager tether and untether methods
+            // throw UnsupportedOperationException, so this cannot happen in normal use. Ensure
+            // that this code cannot run even if callers use raw binder calls or other
+            // unsupported methods.
+            return;
+        }
         mHandler.post(() -> {
             try {
-                listener.onResult(untether(iface));
+                listener.onResult(legacyUntetherInternal(iface));
             } catch (RemoteException e) {
             }
         });
     }
 
-    int untether(String iface) {
+    int legacyUntetherInternal(String iface) {
         if (DBG) Log.d(TAG, "Untethering " + iface);
         TetherState tetherState = mTetherStates.get(iface);
         if (tetherState == null) {
@@ -1045,7 +1264,7 @@
         return TETHER_ERROR_NO_ERROR;
     }
 
-    void untetherAll() {
+    void stopAllTethering() {
         stopTethering(TETHERING_WIFI);
         stopTethering(TETHERING_WIFI_P2P);
         stopTethering(TETHERING_USB);
@@ -1069,22 +1288,6 @@
         return mEntitlementMgr.isTetherProvisioningRequired(cfg);
     }
 
-    private int getRequestedState(int type) {
-        final TetheringRequest request = mActiveTetheringRequests.get(type);
-
-        // The request could have been deleted before we had a chance to complete it.
-        // If so, assume that the scope is the default scope for this tethering type.
-        // This likely doesn't matter - if the request has been deleted, then tethering is
-        // likely going to be stopped soon anyway.
-        final int connectivityScope = (request != null)
-                ? request.getConnectivityScope()
-                : TetheringRequest.getDefaultConnectivityScope(type);
-
-        return connectivityScope == CONNECTIVITY_SCOPE_LOCAL
-                ? IpServer.STATE_LOCAL_ONLY
-                : IpServer.STATE_TETHERED;
-    }
-
     private int getServedUsbType(boolean forNcmFunction) {
         // TETHERING_NCM is only used if the device does not use NCM for regular USB tethering.
         if (forNcmFunction && !mConfig.isUsingNcm()) return TETHERING_NCM;
@@ -1215,7 +1418,7 @@
                 mLog.log("OBSERVED data saver changed");
                 handleDataSaverChanged();
             } else if (action.equals(TetheringNotificationUpdater.ACTION_DISABLE_TETHERING)) {
-                untetherAll();
+                stopAllTethering();
             }
         }
 
@@ -1379,14 +1582,14 @@
 
             mDataSaverEnabled = isDataSaverEnabled;
             if (mDataSaverEnabled) {
-                untetherAll();
+                stopAllTethering();
             }
         }
     }
 
     @VisibleForTesting
-    SparseArray<TetheringRequest> getActiveTetheringRequests() {
-        return mActiveTetheringRequests;
+    SparseArray<TetheringRequest> getPendingTetheringRequests() {
+        return mPendingTetheringRequests;
     }
 
     @VisibleForTesting
@@ -1438,7 +1641,7 @@
                 mNotificationUpdater.notifyTetheringDisabledByRestriction();
 
                 // Untether from all downstreams since tethering is disallowed.
-                mTethering.untetherAll();
+                mTethering.stopAllTethering();
             }
 
             return true;
@@ -1446,14 +1649,17 @@
         }
     }
 
-    private void enableIpServing(int tetheringType, String ifname, int ipServingMode) {
-        enableIpServing(tetheringType, ifname, ipServingMode, false /* isNcm */);
+    final TetheringRequest getPendingTetheringRequest(int type) {
+        return mPendingTetheringRequests.get(type, null);
     }
 
-    private void enableIpServing(int tetheringType, String ifname, int ipServingMode,
-            boolean isNcm) {
-        ensureIpServerStarted(ifname, tetheringType, isNcm);
-        if (tether(ifname, ipServingMode) != TETHER_ERROR_NO_ERROR) {
+    private void enableIpServing(@NonNull TetheringRequest request, String ifname) {
+        enableIpServing(request, ifname, false /* isNcm */);
+    }
+
+    private void enableIpServing(@NonNull TetheringRequest request, String ifname, boolean isNcm) {
+        ensureIpServerStartedForType(ifname, request.getTetheringType(), isNcm);
+        if (tetherInternal(request, ifname) != TETHER_ERROR_NO_ERROR) {
             Log.e(TAG, "unable start tethering on iface " + ifname);
         }
     }
@@ -1505,7 +1711,10 @@
             mLog.e(ifname + " is not a tetherable iface, ignoring");
             return;
         }
-        enableIpServing(type, ifname, IpServer.STATE_LOCAL_ONLY);
+        // No need to call getOrCreatePendingRequest. There can never be explicit requests for
+        // TETHERING_WIFI_P2P because enableTetheringInternal ignores that type.
+        final TetheringRequest request = createImplicitLocalOnlyTetheringRequest(type);
+        enableIpServing(request, ifname);
     }
 
     private void disableWifiP2pIpServingIfNeeded(String ifname) {
@@ -1515,17 +1724,34 @@
         disableWifiIpServingCommon(TETHERING_WIFI_P2P, ifname);
     }
 
+    // TODO: fold this in to enableWifiIpServing.  We cannot do this at the moment because there
+    // are tests that send wifi AP broadcasts with a null interface. But if this can't happen on
+    // real devices, we should fix those tests to always pass in an interface.
+    private int maybeInferWifiTetheringType(String ifname) {
+        return SdkLevel.isAtLeastT() ? TETHERING_WIFI : ifaceNameToType(ifname);
+    }
+
     private void enableWifiIpServing(String ifname, int wifiIpMode) {
         mLog.log("request WiFi tethering - interface=" + ifname + " state=" + wifiIpMode);
 
         // Map wifiIpMode values to IpServer.Callback serving states.
-        final int ipServingMode;
+        TetheringRequest request;
+        final int type;
         switch (wifiIpMode) {
             case IFACE_IP_MODE_TETHERED:
-                ipServingMode = IpServer.STATE_TETHERED;
+                type = maybeInferWifiTetheringType(ifname);
+                request = getOrCreatePendingTetheringRequest(type);
+                // Wifi requests will always have CONNECTIVITY_SCOPE_GLOBAL, because
+                // TetheringRequest.Builder will not allow callers to set CONNECTIVITY_SCOPE_LOCAL
+                // for TETHERING_WIFI. However, if maybeInferWifiTetheringType returns a non-Wifi
+                // type (which could happen on a pre-T implementation of Wi-Fi if the regexps are
+                // misconfigured), then force the connectivity scope to global in order to match the
+                // historical behavior.
+                request.getParcel().connectivityScope = CONNECTIVITY_SCOPE_GLOBAL;
                 break;
             case IFACE_IP_MODE_LOCAL_ONLY:
-                ipServingMode = IpServer.STATE_LOCAL_ONLY;
+                type = maybeInferWifiTetheringType(ifname);
+                request = createImplicitLocalOnlyTetheringRequest(type);
                 break;
             default:
                 mLog.e("Cannot enable IP serving in unknown WiFi mode: " + wifiIpMode);
@@ -1534,14 +1760,13 @@
 
         // After T, tethering always trust the iface pass by state change intent. This allow
         // tethering to deprecate tetherable wifi regexs after T.
-        final int type = SdkLevel.isAtLeastT() ? TETHERING_WIFI : ifaceNameToType(ifname);
         if (!checkTetherableType(type)) {
             mLog.e(ifname + " is not a tetherable iface, ignoring");
             return;
         }
 
         if (!TextUtils.isEmpty(ifname)) {
-            enableIpServing(type, ifname, ipServingMode);
+            enableIpServing(request, ifname);
         } else {
             mLog.e("Cannot enable IP serving on missing interface name");
         }
@@ -1562,7 +1787,6 @@
         // for both TETHERING_USB and TETHERING_NCM, so the local-only NCM interface will be
         // stopped immediately.
         final int tetheringType = getServedUsbType(forNcmFunction);
-        final int requestedState = getRequestedState(tetheringType);
         String[] ifaces = null;
         try {
             ifaces = mNetd.interfaceGetList();
@@ -1571,10 +1795,11 @@
             return;
         }
 
+        final TetheringRequest request = getOrCreatePendingTetheringRequest(tetheringType);
         if (ifaces != null) {
             for (String iface : ifaces) {
                 if (ifaceNameToType(iface) == tetheringType) {
-                    enableIpServing(tetheringType, iface, requestedState, forNcmFunction);
+                    enableIpServing(request, iface, forNcmFunction);
                     return;
                 }
             }
@@ -2085,7 +2310,7 @@
                 }
 
                 mRoutingCoordinator.maybeRemoveDeprecatedUpstreams();
-                mUpstreamNetworkMonitor.startObserveAllNetworks();
+                mUpstreamNetworkMonitor.startObserveUpstreamNetworks();
 
                 // TODO: De-duplicate with updateUpstreamWanted() below.
                 if (upstreamWanted()) {
@@ -2203,9 +2428,14 @@
                         break;
                     }
                     case EVENT_REQUEST_CHANGE_DOWNSTREAM: {
-                        final boolean enabled = message.arg1 == 1;
-                        final TetheringRequest request = (TetheringRequest) message.obj;
-                        enableTetheringInternal(request.getTetheringType(), enabled, null);
+                        final int type = message.arg1;
+                        final Boolean enabled = (Boolean) message.obj;
+                        // Using a placeholder here is ok since we just need to the type of
+                        // tethering to poke the link layer. When the link layer comes up, we won't
+                        // have a pending request to use, but this matches the historical behavior.
+                        // TODO: Get the TetheringRequest from IpServer and make sure to put it in
+                        //       the pending list too.
+                        enableTetheringInternal(enabled, createPlaceholderRequest(type), null);
                         break;
                     }
                     default:
@@ -2783,9 +3013,9 @@
         }
 
         @Override
-        public void requestEnableTethering(TetheringRequest request, boolean enabled) {
+        public void requestEnableTethering(int tetheringType, boolean enabled) {
             mTetherMainSM.sendMessage(TetherMainSM.EVENT_REQUEST_CHANGE_DOWNSTREAM,
-                    enabled ? 1 : 0, 0, request);
+                    tetheringType, 0, enabled ? Boolean.TRUE : Boolean.FALSE);
         }
     }
 
@@ -2806,7 +3036,7 @@
         return type != TETHERING_INVALID;
     }
 
-    private void ensureIpServerStarted(final String iface) {
+    private void ensureIpServerStartedForInterface(final String iface) {
         // If we don't care about this type of interface, ignore.
         final int interfaceType = ifaceNameToType(iface);
         if (!checkTetherableType(interfaceType)) {
@@ -2815,10 +3045,11 @@
             return;
         }
 
-        ensureIpServerStarted(iface, interfaceType, false /* isNcm */);
+        ensureIpServerStartedForType(iface, interfaceType, false /* isNcm */);
     }
 
-    private void ensureIpServerStarted(final String iface, int interfaceType, boolean isNcm) {
+    private void ensureIpServerStartedForType(final String iface, int interfaceType,
+            boolean isNcm) {
         // If we have already started a TISM for this interface, skip.
         if (mTetherStates.containsKey(iface)) {
             mLog.log("active iface (" + iface + ") reported as added, ignoring");
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
index d89bf4d..8e17085 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -37,6 +37,7 @@
 import androidx.annotation.RequiresApi;
 
 import com.android.modules.utils.build.SdkLevel;
+import com.android.net.flags.Flags;
 import com.android.net.module.util.RoutingCoordinatorManager;
 import com.android.net.module.util.RoutingCoordinatorService;
 import com.android.net.module.util.SharedLog;
@@ -208,4 +209,12 @@
     public int getBinderCallingUid() {
         return Binder.getCallingUid();
     }
+
+    /**
+     * Wrapper for tethering_with_soft_ap_config feature flag.
+     */
+    public boolean isTetheringWithSoftApConfigEnabled() {
+        return Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM
+                && Flags.tetheringWithSoftApConfig();
+    }
 }
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringService.java b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
index 3cb5f99..b553f46 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringService.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
@@ -111,7 +111,7 @@
                 IIntResultListener listener) {
             if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return;
 
-            mTethering.tether(iface, IpServer.STATE_TETHERED, listener);
+            mTethering.legacyTether(iface, listener);
         }
 
         @Override
@@ -119,7 +119,7 @@
                 IIntResultListener listener) {
             if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return;
 
-            mTethering.untether(iface, listener);
+            mTethering.legacyUntether(iface, listener);
         }
 
         @Override
@@ -133,9 +133,11 @@
         @Override
         public void startTethering(TetheringRequestParcel request, String callerPkg,
                 String callingAttributionTag, IIntResultListener listener) {
+            boolean onlyAllowPrivileged = request.exemptFromEntitlementCheck
+                    || request.interfaceName != null;
             if (checkAndNotifyCommonError(callerPkg,
                     callingAttributionTag,
-                    request.exemptFromEntitlementCheck /* onlyAllowPrivileged */,
+                    onlyAllowPrivileged,
                     listener)) {
                 return;
             }
@@ -157,6 +159,18 @@
         }
 
         @Override
+        public void stopTetheringRequest(TetheringRequest request,
+                String callerPkg, String callingAttributionTag,
+                IIntResultListener listener) {
+            if (request == null) return;
+            if (listener == null) return;
+            if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return;
+            request.setUid(getBinderCallingUid());
+            request.setPackageName(callerPkg);
+            mTethering.stopTetheringRequest(request, listener);
+        }
+
+        @Override
         public void requestLatestTetheringEntitlementResult(int type, ResultReceiver receiver,
                 boolean showEntitlementUi, String callerPkg, String callingAttributionTag) {
             if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, receiver)) return;
@@ -198,7 +212,7 @@
             if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return;
 
             try {
-                mTethering.untetherAll();
+                mTethering.stopAllTethering();
                 listener.onResult(TETHER_ERROR_NO_ERROR);
             } catch (RemoteException e) { }
         }
@@ -284,6 +298,10 @@
             return false;
         }
 
+        private boolean hasNetworkSettingsPermission() {
+            return checkCallingOrSelfPermission(NETWORK_SETTINGS);
+        }
+
         private boolean hasNetworkStackPermission() {
             return checkCallingOrSelfPermission(NETWORK_STACK)
                     || checkCallingOrSelfPermission(PERMISSION_MAINLINE_NETWORK_STACK);
@@ -299,10 +317,15 @@
 
         private boolean hasTetherChangePermission(final String callerPkg,
                 final String callingAttributionTag, final boolean onlyAllowPrivileged) {
-            if (onlyAllowPrivileged && !hasNetworkStackPermission()) return false;
+            if (onlyAllowPrivileged && !hasNetworkStackPermission()
+                    && !hasNetworkSettingsPermission()) return false;
 
             if (hasTetherPrivilegedPermission()) return true;
 
+            // After TetheringManager moves to public API, prevent third-party apps from being able
+            // to change tethering with only WRITE_SETTINGS permission.
+            if (mTethering.isTetheringWithSoftApConfigEnabled()) return false;
+
             if (mTethering.isTetherProvisioningRequired()) return false;
 
             int uid = getBinderCallingUid();
diff --git a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
index 7a05d74..9705d84 100644
--- a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
+++ b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
@@ -24,6 +24,7 @@
 import static android.net.ConnectivityManager.TYPE_WIFI;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
@@ -44,6 +45,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.SharedLog;
 import com.android.networkstack.apishim.ConnectivityManagerShimImpl;
 import com.android.networkstack.apishim.common.ConnectivityManagerShim;
@@ -62,9 +64,10 @@
  * The owner of UNM gets it to register network callbacks by calling the
  * following methods :
  * Calling #startTrackDefaultNetwork() to track the system default network.
- * Calling #startObserveAllNetworks() to observe all networks. Listening all
- * networks is necessary while the expression of preferred upstreams remains
- * a list of legacy connectivity types.  In future, this can be revisited.
+ * Calling #startObserveUpstreamNetworks() to observe upstream networks.
+ * Listening all upstream networks is necessary while the expression of
+ * preferred upstreams remains a list of legacy connectivity types.
+ * In future, this can be revisited.
  * Calling #setTryCell() to request bringing up mobile DUN or HIPRI.
  *
  * The methods and data members of this class are only to be accessed and
@@ -94,7 +97,7 @@
     @VisibleForTesting
     public static final int TYPE_NONE = -1;
 
-    private static final int CALLBACK_LISTEN_ALL = 1;
+    private static final int CALLBACK_LISTEN_UPSTREAM = 1;
     private static final int CALLBACK_DEFAULT_INTERNET = 2;
     private static final int CALLBACK_MOBILE_REQUEST = 3;
 
@@ -116,7 +119,7 @@
     private HashSet<IpPrefix> mLocalPrefixes;
     private ConnectivityManager mCM;
     private EntitlementManager mEntitlementMgr;
-    private NetworkCallback mListenAllCallback;
+    private NetworkCallback mListenUpstreamCallback;
     private NetworkCallback mDefaultNetworkCallback;
     private NetworkCallback mMobileNetworkCallback;
 
@@ -157,20 +160,29 @@
         }
         ConnectivityManagerShim mCmShim = ConnectivityManagerShimImpl.newInstance(mContext);
         mDefaultNetworkCallback = new UpstreamNetworkCallback(CALLBACK_DEFAULT_INTERNET);
+        // TODO (b/382413665): By definition, a local network cannot be the system default,
+        //  because it does not provide internet capability. Figure out whether this
+        //  is enforced in ConnectivityService. Or what will happen for tethering if it happens.
         mCmShim.registerSystemDefaultNetworkCallback(mDefaultNetworkCallback, mHandler);
         if (mEntitlementMgr == null) {
             mEntitlementMgr = entitle;
         }
     }
 
-    /** Listen all networks. */
-    public void startObserveAllNetworks() {
+    /** Listen upstream networks. */
+    public void startObserveUpstreamNetworks() {
         stop();
 
-        final NetworkRequest listenAllRequest = new NetworkRequest.Builder()
-                .clearCapabilities().build();
-        mListenAllCallback = new UpstreamNetworkCallback(CALLBACK_LISTEN_ALL);
-        cm().registerNetworkCallback(listenAllRequest, mListenAllCallback, mHandler);
+        final NetworkRequest listenUpstreamRequest;
+        // Before V, only TV supports local agent on U, which doesn't support tethering.
+        if (SdkLevel.isAtLeastV()) {
+            listenUpstreamRequest = new NetworkRequest.Builder().clearCapabilities()
+                    .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK).build();
+        }  else {
+            listenUpstreamRequest = new NetworkRequest.Builder().clearCapabilities().build();
+        }
+        mListenUpstreamCallback = new UpstreamNetworkCallback(CALLBACK_LISTEN_UPSTREAM);
+        cm().registerNetworkCallback(listenUpstreamRequest, mListenUpstreamCallback, mHandler);
     }
 
     /**
@@ -183,8 +195,8 @@
     public void stop() {
         setTryCell(false);
 
-        releaseCallback(mListenAllCallback);
-        mListenAllCallback = null;
+        releaseCallback(mListenUpstreamCallback);
+        mListenUpstreamCallback = null;
 
         mNetworkMap.clear();
     }
@@ -535,10 +547,10 @@
                 return;
             }
 
-            // Any non-LISTEN_ALL callback will necessarily concern a network that will
-            // also match the LISTEN_ALL callback by construction of the LISTEN_ALL callback.
-            // So it's not useful to do this work for non-LISTEN_ALL callbacks.
-            if (mCallbackType == CALLBACK_LISTEN_ALL) {
+            // Any non-LISTEN_UPSTREAM callback will necessarily concern a network that will
+            // also match the LISTEN_UPSTREAM callback by construction of the LISTEN_UPSTREAM
+            // callback. So it's not useful to do this work for non-LISTEN_UPSTREAM callbacks.
+            if (mCallbackType == CALLBACK_LISTEN_UPSTREAM) {
                 recomputeLocalPrefixes();
             }
         }
@@ -555,10 +567,11 @@
             }
 
             handleLost(network);
-            // Any non-LISTEN_ALL callback will necessarily concern a network that will
-            // also match the LISTEN_ALL callback by construction of the LISTEN_ALL callback.
-            // So it's not useful to do this work for non-LISTEN_ALL callbacks.
-            if (mCallbackType == CALLBACK_LISTEN_ALL) {
+            // Any non-LISTEN_UPSTREAM callback will necessarily concern a network that will
+            // also match the LISTEN_UPSTREAM callback by construction of the
+            // LISTEN_UPSTREAM callback. So it's not useful to do this work for
+            // non-LISTEN_UPSTREAM callbacks.
+            if (mCallbackType == CALLBACK_LISTEN_UPSTREAM) {
                 recomputeLocalPrefixes();
             }
         }
diff --git a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
index 01f3af9..1323f28 100644
--- a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
+++ b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
@@ -332,15 +332,16 @@
         // seconds. See b/289881008.
         private static final int EXPANDED_TIMEOUT_MS = 30000;
 
-        MyTetheringEventCallback(String iface) {
-            mIface = new TetheringInterface(TETHERING_ETHERNET, iface);
+        MyTetheringEventCallback(int tetheringType, String iface) {
+            mIface = new TetheringInterface(tetheringType, iface);
             mExpectedUpstream = null;
             mAcceptAnyUpstream = true;
         }
 
-        MyTetheringEventCallback(String iface, @NonNull Network expectedUpstream) {
+        MyTetheringEventCallback(
+                int tetheringType, String iface, @NonNull Network expectedUpstream) {
             Objects.requireNonNull(expectedUpstream);
-            mIface = new TetheringInterface(TETHERING_ETHERNET, iface);
+            mIface = new TetheringInterface(tetheringType, iface);
             mExpectedUpstream = expectedUpstream;
             mAcceptAnyUpstream = false;
         }
@@ -392,12 +393,12 @@
         }
 
         public void awaitInterfaceTethered() throws Exception {
-            assertTrue("Ethernet not tethered after " + EXPANDED_TIMEOUT_MS + "ms",
+            assertTrue("Interface is not tethered after " + EXPANDED_TIMEOUT_MS + "ms",
                     mTetheringStartedLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS));
         }
 
         public void awaitInterfaceLocalOnly() throws Exception {
-            assertTrue("Ethernet not local-only after " + EXPANDED_TIMEOUT_MS + "ms",
+            assertTrue("Interface is not local-only after " + EXPANDED_TIMEOUT_MS + "ms",
                     mLocalOnlyStartedLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS));
         }
 
@@ -501,15 +502,17 @@
         sCallbackErrors.add(error);
     }
 
-    protected static MyTetheringEventCallback enableEthernetTethering(String iface,
+    protected static MyTetheringEventCallback enableTethering(String iface,
             TetheringRequest request, Network expectedUpstream) throws Exception {
-        // Enable ethernet tethering with null expectedUpstream means the test accept any upstream
-        // after etherent tethering started.
+        // Enable tethering with null expectedUpstream means the test accept any upstream after
+        // tethering started.
         final MyTetheringEventCallback callback;
         if (expectedUpstream != null) {
-            callback = new MyTetheringEventCallback(iface, expectedUpstream);
+            callback =
+                    new MyTetheringEventCallback(
+                            request.getTetheringType(), iface, expectedUpstream);
         } else {
-            callback = new MyTetheringEventCallback(iface);
+            callback = new MyTetheringEventCallback(request.getTetheringType(), iface);
         }
         runAsShell(NETWORK_SETTINGS, () -> {
             sTm.registerTetheringEventCallback(c -> c.run() /* executor */, callback);
@@ -521,7 +524,7 @@
         StartTetheringCallback startTetheringCallback = new StartTetheringCallback() {
             @Override
             public void onTetheringStarted() {
-                Log.d(TAG, "Ethernet tethering started");
+                Log.d(TAG, "Tethering started");
                 tetheringStartedLatch.countDown();
             }
 
@@ -530,8 +533,8 @@
                 addCallbackError("Unexpectedly got onTetheringFailed");
             }
         };
-        Log.d(TAG, "Starting Ethernet tethering");
-        runAsShell(TETHER_PRIVILEGED, () -> {
+        Log.d(TAG, "Starting tethering");
+        runAsShell(TETHER_PRIVILEGED, NETWORK_SETTINGS, () -> {
             sTm.startTethering(request, c -> c.run() /* executor */, startTetheringCallback);
             // Binder call is an async call. Need to hold the shell permission until tethering
             // started. This helps to avoid the test become flaky.
@@ -557,7 +560,7 @@
 
     protected static MyTetheringEventCallback enableEthernetTethering(String iface,
             Network expectedUpstream) throws Exception {
-        return enableEthernetTethering(iface,
+        return enableTethering(iface,
                 new TetheringRequest.Builder(TETHERING_ETHERNET)
                 .setShouldShowEntitlementUi(false).build(), expectedUpstream);
     }
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index 1bbea94..5c8d347 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -19,7 +19,9 @@
 import static android.Manifest.permission.DUMP;
 import static android.net.InetAddresses.parseNumericAddress;
 import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
 import static android.net.TetheringManager.TETHERING_ETHERNET;
+import static android.net.TetheringManager.TETHERING_VIRTUAL;
 import static android.net.TetheringTester.TestDnsPacket;
 import static android.net.TetheringTester.buildIcmpEchoPacketV4;
 import static android.net.TetheringTester.buildUdpPacket;
@@ -236,7 +238,8 @@
             downstreamReader = makePacketReader(fd, mtu);
             tetheringEventCallback = enableEthernetTethering(downstreamIface.getInterfaceName(),
                     null /* any upstream */);
-            checkTetheredClientCallbacks(downstreamReader, tetheringEventCallback);
+            checkTetheredClientCallbacks(
+                    downstreamReader, TETHERING_ETHERNET, tetheringEventCallback);
         } finally {
             maybeStopTapPacketReader(downstreamReader);
             maybeCloseTestInterface(downstreamIface);
@@ -267,7 +270,8 @@
             downstreamReader = makePacketReader(fd, getMTU(downstreamIface));
             tetheringEventCallback = enableEthernetTethering(downstreamIface.getInterfaceName(),
                     null /* any upstream */);
-            checkTetheredClientCallbacks(downstreamReader, tetheringEventCallback);
+            checkTetheredClientCallbacks(
+                    downstreamReader, TETHERING_ETHERNET, tetheringEventCallback);
         } finally {
             maybeStopTapPacketReader(downstreamReader);
             maybeCloseTestInterface(downstreamIface);
@@ -302,7 +306,7 @@
 
             final String localAddr = "192.0.2.3/28";
             final String clientAddr = "192.0.2.2/28";
-            tetheringEventCallback = enableEthernetTethering(iface,
+            tetheringEventCallback = enableTethering(iface,
                     requestWithStaticIpv4(localAddr, clientAddr), null /* any upstream */);
 
             tetheringEventCallback.awaitInterfaceTethered();
@@ -368,8 +372,7 @@
 
             final TetheringRequest request = new TetheringRequest.Builder(TETHERING_ETHERNET)
                     .setConnectivityScope(CONNECTIVITY_SCOPE_LOCAL).build();
-            tetheringEventCallback = enableEthernetTethering(iface, request,
-                    null /* any upstream */);
+            tetheringEventCallback = enableTethering(iface, request, null /* any upstream */);
             tetheringEventCallback.awaitInterfaceLocalOnly();
 
             // makePacketReader only works after tethering is started, because until then the
@@ -424,6 +427,7 @@
     }
 
     private void checkTetheredClientCallbacks(final PollPacketReader packetReader,
+            final int tetheringType,
             final MyTetheringEventCallback tetheringEventCallback) throws Exception {
         // Create a fake client.
         byte[] clientMacAddr = new byte[6];
@@ -438,7 +442,7 @@
 
         // Check the MAC address.
         assertEquals(MacAddress.fromBytes(clientMacAddr), client.getMacAddress());
-        assertEquals(TETHERING_ETHERNET, client.getTetheringType());
+        assertEquals(tetheringType, client.getTetheringType());
 
         // Check the hostname.
         assertEquals(1, client.getAddresses().size());
@@ -475,8 +479,7 @@
     private void assertInvalidStaticIpv4Request(String iface, String local, String client)
             throws Exception {
         try {
-            enableEthernetTethering(iface, requestWithStaticIpv4(local, client),
-                    null /* any upstream */);
+            enableTethering(iface, requestWithStaticIpv4(local, client), null /* any upstream */);
             fail("Unexpectedly accepted invalid IPv4 configuration: " + local + ", " + client);
         } catch (IllegalArgumentException | NullPointerException expected) { }
     }
@@ -1180,4 +1183,32 @@
                 TX_UDP_PACKET_COUNT * (TX_UDP_PACKET_SIZE + IPV6_HEADER_LEN - IPV4_HEADER_MIN_LEN),
                 newEgress4.bytes - oldEgress4.bytes);
     }
+
+    @Test
+    public void testTetheringVirtual() throws Exception {
+        assumeFalse(isInterfaceForTetheringAvailable());
+        setIncludeTestInterfaces(true);
+
+        TestNetworkInterface downstreamIface = null;
+        MyTetheringEventCallback tetheringEventCallback = null;
+        PollPacketReader downstreamReader = null;
+        try {
+            downstreamIface = createTestInterface();
+            String iface = downstreamIface.getInterfaceName();
+            final TetheringRequest request = new TetheringRequest.Builder(TETHERING_VIRTUAL)
+                    .setConnectivityScope(CONNECTIVITY_SCOPE_GLOBAL)
+                    .setInterfaceName(iface)
+                    .build();
+            tetheringEventCallback = enableTethering(iface, request, null /* any upstream */);
+
+            FileDescriptor fd = downstreamIface.getFileDescriptor().getFileDescriptor();
+            downstreamReader = makePacketReader(fd, getMTU(downstreamIface));
+            checkTetheredClientCallbacks(
+                    downstreamReader, TETHERING_VIRTUAL, tetheringEventCallback);
+        } finally {
+            maybeStopTapPacketReader(downstreamReader);
+            maybeCloseTestInterface(downstreamIface);
+            maybeUnregisterTetheringEventCallback(tetheringEventCallback);
+        }
+    }
 }
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index 680e81d..84b301f 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -48,7 +48,6 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyBoolean;
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.clearInvocations;
@@ -74,6 +73,7 @@
 import android.net.LinkProperties;
 import android.net.MacAddress;
 import android.net.RouteInfo;
+import android.net.TetheringManager.TetheringRequest;
 import android.net.dhcp.DhcpServerCallbacks;
 import android.net.dhcp.DhcpServingParamsParcel;
 import android.net.dhcp.IDhcpEventCallbacks;
@@ -240,7 +240,8 @@
             Set<LinkAddress> upstreamAddresses, boolean usingLegacyDhcp, boolean usingBpfOffload)
             throws Exception {
         initStateMachine(interfaceType, usingLegacyDhcp, usingBpfOffload);
-        dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+        dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+                createMockTetheringRequest(CONNECTIVITY_SCOPE_GLOBAL));
         verify(mBpfCoordinator).addIpServer(mIpServer);
         if (upstreamIface != null) {
             InterfaceParams interfaceParams = mDependencies.getInterfaceParams(upstreamIface);
@@ -345,7 +346,8 @@
     public void canBeTetheredAsBluetooth() throws Exception {
         initStateMachine(TETHERING_BLUETOOTH);
 
-        dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+        dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+                createMockTetheringRequest(CONNECTIVITY_SCOPE_GLOBAL));
         InOrder inOrder = inOrder(mCallback, mNetd, mRoutingCoordinatorManager);
         if (isAtLeastT()) {
             inOrder.verify(mRoutingCoordinatorManager)
@@ -400,7 +402,8 @@
     public void canBeTetheredAsUsb() throws Exception {
         initStateMachine(TETHERING_USB);
 
-        dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+        dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+                createMockTetheringRequest(CONNECTIVITY_SCOPE_GLOBAL));
         InOrder inOrder = inOrder(mCallback, mNetd, mRoutingCoordinatorManager);
         inOrder.verify(mRoutingCoordinatorManager).requestStickyDownstreamAddress(anyInt(),
                 eq(CONNECTIVITY_SCOPE_GLOBAL), any());
@@ -423,7 +426,8 @@
     public void canBeTetheredAsWifiP2p_NotUsingDedicatedIp() throws Exception {
         initStateMachine(TETHERING_WIFI_P2P);
 
-        dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY);
+        dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+                createMockTetheringRequest(CONNECTIVITY_SCOPE_LOCAL));
         InOrder inOrder = inOrder(mCallback, mNetd, mRoutingCoordinatorManager);
         inOrder.verify(mRoutingCoordinatorManager).requestStickyDownstreamAddress(anyInt(),
                 eq(CONNECTIVITY_SCOPE_LOCAL), any());
@@ -447,7 +451,8 @@
         initStateMachine(TETHERING_WIFI_P2P, false /* usingLegacyDhcp */, DEFAULT_USING_BPF_OFFLOAD,
                 true /* shouldEnableWifiP2pDedicatedIp */);
 
-        dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY);
+        dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+                createMockTetheringRequest(CONNECTIVITY_SCOPE_LOCAL));
         InOrder inOrder = inOrder(mCallback, mNetd, mRoutingCoordinatorManager);
         // When using WiFi P2p dedicated IP, the IpServer just picks the IP address without
         // requesting for it at RoutingCoordinatorManager.
@@ -627,7 +632,8 @@
         initStateMachine(TETHERING_USB);
 
         doThrow(RemoteException.class).when(mNetd).tetherInterfaceAdd(IFACE_NAME);
-        dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+        dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+                createMockTetheringRequest(CONNECTIVITY_SCOPE_GLOBAL));
         InOrder usbTeardownOrder = inOrder(mNetd, mCallback);
         usbTeardownOrder.verify(mNetd).interfaceSetCfg(
                 argThat(cfg -> IFACE_NAME.equals(cfg.ifName)));
@@ -713,7 +719,8 @@
     @Test
     public void startsDhcpServerOnNcm() throws Exception {
         initStateMachine(TETHERING_NCM);
-        dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY);
+        dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+                createMockTetheringRequest(CONNECTIVITY_SCOPE_LOCAL));
         dispatchTetherConnectionChanged(UPSTREAM_IFACE);
 
         assertDhcpStarted(new IpPrefix("192.168.42.0/24"));
@@ -722,7 +729,8 @@
     @Test
     public void testOnNewPrefixRequest() throws Exception {
         initStateMachine(TETHERING_NCM);
-        dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY);
+        dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+                createMockTetheringRequest(CONNECTIVITY_SCOPE_LOCAL));
 
         final IDhcpEventCallbacks eventCallbacks;
         final ArgumentCaptor<IDhcpEventCallbacks> dhcpEventCbsCaptor =
@@ -911,7 +919,8 @@
         doNothing().when(mDependencies).makeDhcpServer(any(), mDhcpParamsCaptor.capture(),
                 cbCaptor.capture());
         initStateMachine(TETHERING_WIFI);
-        dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+        dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+                createMockTetheringRequest(CONNECTIVITY_SCOPE_GLOBAL));
         verify(mDhcpServer, never()).startWithCallbacks(any(), any());
 
         // No stop dhcp server because dhcp server is not created yet.
@@ -957,14 +966,22 @@
         assertDhcpServingParams(mDhcpParamsCaptor.getValue(), expectedPrefix);
     }
 
+    private TetheringRequest createMockTetheringRequest(int connectivityScope) {
+        TetheringRequest request = mock(TetheringRequest.class);
+        when(request.getConnectivityScope()).thenReturn(connectivityScope);
+        return request;
+    }
+
     /**
      * Send a command to the state machine under test, and run the event loop to idle.
      *
      * @param command One of the IpServer.CMD_* constants.
-     * @param arg1 An additional argument to pass.
+     * @param arg1    An additional argument to pass.
+     * @param arg2    An additional argument to pass.
+     * @param obj     An additional object to pass.
      */
-    private void dispatchCommand(int command, int arg1) {
-        mIpServer.sendMessage(command, arg1);
+    private void dispatchCommand(int command, int arg1, int arg2, Object obj) {
+        mIpServer.sendMessage(command, arg1, arg2, obj);
         mLooper.dispatchAll();
     }
 
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
index 51c2d56..58e1894 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
@@ -50,6 +50,7 @@
 import static org.mockito.Matchers.anyLong;
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -160,10 +161,20 @@
             return super.getSystemServiceName(serviceClass);
         }
 
+        @NonNull
         @Override
         public Context createContextAsUser(UserHandle user, int flags) {
+            if (mCreateContextAsUserException != null) {
+                throw mCreateContextAsUserException;
+            }
             return mMockContext; // Return self for easier test injection.
         }
+
+        private RuntimeException mCreateContextAsUserException = null;
+
+        private void setCreateContextAsUserException(RuntimeException e) {
+            mCreateContextAsUserException = e;
+        }
     }
 
     class TestDependencies extends EntitlementManager.Dependencies {
@@ -594,6 +605,14 @@
 
     @IgnoreUpTo(SC_V2)
     @Test
+    public void testUiProvisioningMultiUser_aboveT_createContextAsUserThrows() {
+        mMockContext.setCreateContextAsUserException(new IllegalStateException());
+        doTestUiProvisioningMultiUser(true, 1);
+        doTestUiProvisioningMultiUser(false, 1);
+    }
+
+    @IgnoreUpTo(SC_V2)
+    @Test
     public void testUiProvisioningMultiUser_aboveT() {
         doTestUiProvisioningMultiUser(true, 1);
         doTestUiProvisioningMultiUser(false, 0);
@@ -639,6 +658,7 @@
         doReturn(isAdminUser).when(mUserManager).isAdminUser();
 
         mDeps.reset();
+        clearInvocations(mTetherProvisioningFailedListener);
         mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
         mEnMgr.notifyUpstream(true);
         mLooper.dispatchAll();
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
index 1608e1a..f9e3a6a 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
@@ -186,7 +186,8 @@
         // - Test bluetooth prefix is reserved.
         when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(
                 getSubAddress(mBluetoothAddress.getAddress().getAddress()));
-        final LinkAddress hotspotAddress = requestDownstreamAddress(mHotspotIpServer);
+        final LinkAddress hotspotAddress = requestStickyDownstreamAddress(mHotspotIpServer,
+                CONNECTIVITY_SCOPE_GLOBAL);
         final IpPrefix hotspotPrefix = asIpPrefix(hotspotAddress);
         assertNotEquals(asIpPrefix(mBluetoothAddress), hotspotPrefix);
         releaseDownstream(mHotspotIpServer);
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java
index b2cbf75..51ba140 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java
@@ -18,6 +18,7 @@
 
 import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 
 import static com.android.networkstack.apishim.common.ShimUtils.isAtLeastS;
@@ -41,6 +42,8 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.modules.utils.build.SdkLevel;
+
 import java.util.Map;
 import java.util.Objects;
 
@@ -119,12 +122,15 @@
                 && mLegacyTypeMap.isEmpty();
     }
 
-    boolean isListeningForAll() {
-        final NetworkCapabilities empty = new NetworkCapabilities();
-        empty.clearAll();
+    boolean isListeningForUpstream() {
+        final NetworkCapabilities upstreamNc = new NetworkCapabilities();
+        upstreamNc.clearAll();
+        if (SdkLevel.isAtLeastV()) {
+            upstreamNc.addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK);
+        }
 
         for (NetworkRequestInfo nri : mListening.values()) {
-            if (nri.request.networkCapabilities.equalRequestableCapabilities(empty)) {
+            if (nri.request.networkCapabilities.equalRequestableCapabilities(upstreamNc)) {
                 return true;
             }
         }
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 0dbf772..b550ada 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
@@ -17,9 +17,11 @@
 package com.android.networkstack.tethering;
 
 import static android.Manifest.permission.ACCESS_NETWORK_STATE;
+import static android.Manifest.permission.NETWORK_SETTINGS;
 import static android.Manifest.permission.TETHER_PRIVILEGED;
 import static android.Manifest.permission.WRITE_SETTINGS;
 import static android.content.pm.PackageManager.PERMISSION_DENIED;
+import static android.net.TetheringManager.TETHERING_VIRTUAL;
 import static android.net.TetheringManager.TETHERING_WIFI;
 import static android.net.TetheringManager.TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION;
 import static android.net.TetheringManager.TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION;
@@ -51,7 +53,6 @@
 import android.net.TetheringManager;
 import android.net.TetheringManager.TetheringRequest;
 import android.net.TetheringRequestParcel;
-import android.net.ip.IpServer;
 import android.os.Bundle;
 import android.os.ConditionVariable;
 import android.os.Handler;
@@ -152,27 +153,43 @@
     }
 
     private void runAsNoPermission(final TestTetheringCall test) throws Exception {
-        runTetheringCall(test, true /* isTetheringAllowed */, new String[0]);
+        runTetheringCall(test, true /* isTetheringAllowed */, false /* isWriteSettingsAllowed */,
+                new String[0]);
     }
 
     private void runAsTetherPrivileged(final TestTetheringCall test) throws Exception {
-        runTetheringCall(test, true /* isTetheringAllowed */, TETHER_PRIVILEGED);
+        runTetheringCall(test, true /* isTetheringAllowed */, false /* isWriteSettingsAllowed */,
+                TETHER_PRIVILEGED);
     }
 
     private void runAsAccessNetworkState(final TestTetheringCall test) throws Exception {
-        runTetheringCall(test, true /* isTetheringAllowed */, ACCESS_NETWORK_STATE);
+        runTetheringCall(test, true /* isTetheringAllowed */, false /* isWriteSettingsAllowed */,
+                ACCESS_NETWORK_STATE);
     }
 
     private void runAsWriteSettings(final TestTetheringCall test) throws Exception {
-        runTetheringCall(test, true /* isTetheringAllowed */, WRITE_SETTINGS);
+        runTetheringCall(test, true /* isTetheringAllowed */, false /* isWriteSettingsAllowed */,
+                WRITE_SETTINGS);
+    }
+
+    private void runAsWriteSettingsWhenWriteSettingsAllowed(
+            final TestTetheringCall test) throws Exception {
+        runTetheringCall(test, true /* isTetheringAllowed */, true /* isWriteSettingsAllowed */,
+                WRITE_SETTINGS);
     }
 
     private void runAsTetheringDisallowed(final TestTetheringCall test) throws Exception {
-        runTetheringCall(test, false /* isTetheringAllowed */, TETHER_PRIVILEGED);
+        runTetheringCall(test, false /* isTetheringAllowed */, false /* isWriteSettingsAllowed */,
+                TETHER_PRIVILEGED);
+    }
+
+    private void runAsNetworkSettings(final TestTetheringCall test) throws Exception {
+        runTetheringCall(test, true /* isTetheringAllowed */, false /* isWriteSettingsAllowed */,
+                NETWORK_SETTINGS, TETHER_PRIVILEGED);
     }
 
     private void runTetheringCall(final TestTetheringCall test, boolean isTetheringAllowed,
-            String... permissions) throws Exception {
+            boolean isWriteSettingsAllowed, String... permissions) throws Exception {
         // Allow the test to run even if ACCESS_NETWORK_STATE was granted at the APK level
         if (!CollectionUtils.contains(permissions, ACCESS_NETWORK_STATE)) {
             mMockConnector.setPermission(ACCESS_NETWORK_STATE, PERMISSION_DENIED);
@@ -182,6 +199,8 @@
         try {
             when(mTethering.isTetheringSupported()).thenReturn(true);
             when(mTethering.isTetheringAllowed()).thenReturn(isTetheringAllowed);
+            when(mTethering.isTetheringWithSoftApConfigEnabled())
+                    .thenReturn(!isWriteSettingsAllowed);
             test.runTetheringCall(new TestTetheringResult());
         } finally {
             mUiAutomation.dropShellPermissionIdentity();
@@ -199,7 +218,7 @@
         mTetheringConnector.tether(TEST_IFACE_NAME, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG, result);
         verify(mTethering).isTetheringSupported();
         verify(mTethering).isTetheringAllowed();
-        verify(mTethering).tether(TEST_IFACE_NAME, IpServer.STATE_TETHERED, result);
+        verify(mTethering).legacyTether(TEST_IFACE_NAME, result);
     }
 
     @Test
@@ -207,7 +226,7 @@
         runAsNoPermission((result) -> {
             mTetheringConnector.tether(TEST_IFACE_NAME, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
                     result);
-            verify(mTethering).isTetherProvisioningRequired();
+            verify(mTethering).isTetheringWithSoftApConfigEnabled();
             result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
             verifyNoMoreInteractionsForTethering();
         });
@@ -218,7 +237,16 @@
         });
 
         runAsWriteSettings((result) -> {
+            mTetheringConnector.tether(TEST_IFACE_NAME, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
+                    result);
+            verify(mTethering).isTetheringWithSoftApConfigEnabled();
+            result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+            verifyNoMoreInteractionsForTethering();
+        });
+
+        runAsWriteSettingsWhenWriteSettingsAllowed((result) -> {
             runTether(result);
+            verify(mTethering).isTetheringWithSoftApConfigEnabled();
             verify(mTethering).isTetherProvisioningRequired();
             verifyNoMoreInteractionsForTethering();
         });
@@ -238,7 +266,7 @@
                 result);
         verify(mTethering).isTetheringSupported();
         verify(mTethering).isTetheringAllowed();
-        verify(mTethering).untether(eq(TEST_IFACE_NAME), eq(result));
+        verify(mTethering).legacyUntether(eq(TEST_IFACE_NAME), eq(result));
     }
 
     @Test
@@ -246,7 +274,7 @@
         runAsNoPermission((result) -> {
             mTetheringConnector.untether(TEST_IFACE_NAME, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
                     result);
-            verify(mTethering).isTetherProvisioningRequired();
+            verify(mTethering).isTetheringWithSoftApConfigEnabled();
             result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
             verifyNoMoreInteractionsForTethering();
         });
@@ -257,7 +285,16 @@
         });
 
         runAsWriteSettings((result) -> {
+            mTetheringConnector.untether(TEST_IFACE_NAME, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
+                    result);
+            verify(mTethering).isTetheringWithSoftApConfigEnabled();
+            result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+            verifyNoMoreInteractionsForTethering();
+        });
+
+        runAsWriteSettingsWhenWriteSettingsAllowed((result) -> {
             runUnTether(result);
+            verify(mTethering).isTetheringWithSoftApConfigEnabled();
             verify(mTethering).isTetherProvisioningRequired();
             verifyNoMoreInteractionsForTethering();
         });
@@ -291,7 +328,7 @@
         runAsNoPermission((result) -> {
             mTetheringConnector.setUsbTethering(true /* enable */, TEST_CALLER_PKG,
                     TEST_ATTRIBUTION_TAG, result);
-            verify(mTethering).isTetherProvisioningRequired();
+            verify(mTethering).isTetheringWithSoftApConfigEnabled();
             result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
             verifyNoMoreInteractionsForTethering();
         });
@@ -302,7 +339,16 @@
         });
 
         runAsWriteSettings((result) -> {
+            mTetheringConnector.setUsbTethering(true /* enable */, TEST_CALLER_PKG,
+                    TEST_ATTRIBUTION_TAG, result);
+            verify(mTethering).isTetheringWithSoftApConfigEnabled();
+            result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+            verifyNoMoreInteractionsForTethering();
+        });
+
+        runAsWriteSettingsWhenWriteSettingsAllowed((result) -> {
             runSetUsbTethering(result);
+            verify(mTethering).isTetheringWithSoftApConfigEnabled();
             verify(mTethering).isTetherProvisioningRequired();
             verifyNoMoreInteractionsForTethering();
         });
@@ -335,7 +381,7 @@
         runAsNoPermission((result) -> {
             mTetheringConnector.startTethering(request, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
                     result);
-            verify(mTethering).isTetherProvisioningRequired();
+            verify(mTethering).isTetheringWithSoftApConfigEnabled();
             result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
             verifyNoMoreInteractionsForTethering();
         });
@@ -355,7 +401,16 @@
         });
 
         runAsWriteSettings((result) -> {
+            mTetheringConnector.startTethering(request, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
+                    result);
+            verify(mTethering).isTetheringWithSoftApConfigEnabled();
+            result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+            verifyNoMoreInteractionsForTethering();
+        });
+
+        runAsWriteSettingsWhenWriteSettingsAllowed((result) -> {
             runStartTethering(result, request);
+            verify(mTethering).isTetheringWithSoftApConfigEnabled();
             verify(mTethering).isTetherProvisioningRequired();
             verifyNoMoreInteractionsForTethering();
         });
@@ -370,6 +425,32 @@
         });
     }
 
+    @Test
+    public void testStartTetheringWithInterfaceSucceeds() throws Exception {
+        final TetheringRequestParcel request = new TetheringRequestParcel();
+        request.tetheringType = TETHERING_VIRTUAL;
+        request.interfaceName = "avf_tap_fixed";
+
+        runAsNetworkSettings((result) -> {
+            runStartTethering(result, request);
+            verifyNoMoreInteractionsForTethering();
+        });
+    }
+
+    @Test
+    public void testStartTetheringNoNetworkStackPermissionWithInterfaceFails() throws Exception {
+        final TetheringRequestParcel request = new TetheringRequestParcel();
+        request.tetheringType = TETHERING_VIRTUAL;
+        request.interfaceName = "avf_tap_fixed";
+
+        runAsTetherPrivileged((result) -> {
+            mTetheringConnector.startTethering(request, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
+                    result);
+            result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+            verifyNoMoreInteractionsForTethering();
+        });
+    }
+
     private void runStartTetheringAndVerifyNoPermission(final TestTetheringResult result)
             throws Exception {
         final TetheringRequestParcel request = new TetheringRequestParcel();
@@ -414,7 +495,7 @@
         runAsNoPermission((result) -> {
             mTetheringConnector.stopTethering(TETHERING_WIFI, TEST_CALLER_PKG,
                     TEST_ATTRIBUTION_TAG, result);
-            verify(mTethering).isTetherProvisioningRequired();
+            verify(mTethering).isTetheringWithSoftApConfigEnabled();
             result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
             verifyNoMoreInteractionsForTethering();
         });
@@ -425,7 +506,16 @@
         });
 
         runAsWriteSettings((result) -> {
+            mTetheringConnector.stopTethering(TETHERING_WIFI, TEST_CALLER_PKG,
+                    TEST_ATTRIBUTION_TAG, result);
+            verify(mTethering).isTetheringWithSoftApConfigEnabled();
+            result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+            verifyNoMoreInteractionsForTethering();
+        });
+
+        runAsWriteSettingsWhenWriteSettingsAllowed((result) -> {
             runStopTethering(result);
+            verify(mTethering).isTetheringWithSoftApConfigEnabled();
             verify(mTethering).isTetherProvisioningRequired();
             verifyNoMoreInteractionsForTethering();
         });
@@ -454,11 +544,13 @@
     public void testRequestLatestTetheringEntitlementResult() throws Exception {
         // Run as no permission.
         final MyResultReceiver result = new MyResultReceiver(null);
-        mTetheringConnector.requestLatestTetheringEntitlementResult(TETHERING_WIFI, result,
-                true /* showEntitlementUi */, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG);
-        verify(mTethering).isTetherProvisioningRequired();
-        result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
-        verifyNoMoreInteractions(mTethering);
+        runAsNoPermission((none) -> {
+            mTetheringConnector.requestLatestTetheringEntitlementResult(TETHERING_WIFI, result,
+                    true /* showEntitlementUi */, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG);
+            verify(mTethering).isTetheringWithSoftApConfigEnabled();
+            result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+            verifyNoMoreInteractionsForTethering();
+        });
 
         runAsTetherPrivileged((none) -> {
             runRequestLatestTetheringEntitlementResult();
@@ -469,22 +561,30 @@
             mTetheringConnector.requestLatestTetheringEntitlementResult(TETHERING_WIFI, result,
                     true /* showEntitlementUi */, TEST_WRONG_PACKAGE, TEST_ATTRIBUTION_TAG);
             result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
-            verifyNoMoreInteractions(mTethering);
+            verifyNoMoreInteractionsForTethering();
         });
 
         runAsWriteSettings((none) -> {
+            mTetheringConnector.requestLatestTetheringEntitlementResult(TETHERING_WIFI, result,
+                    true /* showEntitlementUi */, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG);
+            verify(mTethering).isTetheringWithSoftApConfigEnabled();
+            result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+            verifyNoMoreInteractionsForTethering();
+        });
+
+        runAsWriteSettingsWhenWriteSettingsAllowed((none) -> {
             runRequestLatestTetheringEntitlementResult();
+            verify(mTethering).isTetheringWithSoftApConfigEnabled();
             verify(mTethering).isTetherProvisioningRequired();
             verifyNoMoreInteractionsForTethering();
         });
 
         runAsTetheringDisallowed((none) -> {
-            final MyResultReceiver receiver = new MyResultReceiver(null);
-            mTetheringConnector.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver,
+            mTetheringConnector.requestLatestTetheringEntitlementResult(TETHERING_WIFI, result,
                     true /* showEntitlementUi */, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG);
             verify(mTethering).isTetheringSupported();
             verify(mTethering).isTetheringAllowed();
-            receiver.assertResult(TETHER_ERROR_UNSUPPORTED);
+            result.assertResult(TETHER_ERROR_UNSUPPORTED);
             verifyNoMoreInteractionsForTethering();
         });
     }
@@ -560,7 +660,7 @@
         mTetheringConnector.stopAllTethering(TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG, result);
         verify(mTethering).isTetheringSupported();
         verify(mTethering).isTetheringAllowed();
-        verify(mTethering).untetherAll();
+        verify(mTethering).stopAllTethering();
         result.assertResult(TETHER_ERROR_NO_ERROR);
     }
 
@@ -568,7 +668,7 @@
     public void testStopAllTethering() throws Exception {
         runAsNoPermission((result) -> {
             mTetheringConnector.stopAllTethering(TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG, result);
-            verify(mTethering).isTetherProvisioningRequired();
+            verify(mTethering).isTetheringWithSoftApConfigEnabled();
             result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
             verifyNoMoreInteractionsForTethering();
         });
@@ -579,7 +679,15 @@
         });
 
         runAsWriteSettings((result) -> {
+            mTetheringConnector.stopAllTethering(TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG, result);
+            verify(mTethering).isTetheringWithSoftApConfigEnabled();
+            result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+            verifyNoMoreInteractionsForTethering();
+        });
+
+        runAsWriteSettingsWhenWriteSettingsAllowed((result) -> {
             runStopAllTethering(result);
+            verify(mTethering).isTetheringWithSoftApConfigEnabled();
             verify(mTethering).isTetherProvisioningRequired();
             verifyNoMoreInteractionsForTethering();
         });
@@ -605,7 +713,7 @@
         runAsNoPermission((result) -> {
             mTetheringConnector.isTetheringSupported(TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
                     result);
-            verify(mTethering).isTetherProvisioningRequired();
+            verify(mTethering).isTetheringWithSoftApConfigEnabled();
             result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
             verifyNoMoreInteractionsForTethering();
         });
@@ -616,7 +724,16 @@
         });
 
         runAsWriteSettings((result) -> {
+            mTetheringConnector.isTetheringSupported(TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
+                    result);
+            verify(mTethering).isTetheringWithSoftApConfigEnabled();
+            result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+            verifyNoMoreInteractionsForTethering();
+        });
+
+        runAsWriteSettingsWhenWriteSettingsAllowed((result) -> {
             runIsTetheringSupported(result);
+            verify(mTethering).isTetheringWithSoftApConfigEnabled();
             verify(mTethering).isTetherProvisioningRequired();
             verifyNoMoreInteractionsForTethering();
         });
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 17f5081..e1c2db9 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -47,6 +47,7 @@
 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.TETHER_ERROR_IFACE_CFG_ERROR;
@@ -92,7 +93,6 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.notNull;
@@ -255,6 +255,7 @@
     private static final String TEST_P2P_IFNAME = "test_p2p-p2p0-0";
     private static final String TEST_NCM_IFNAME = "test_ncm0";
     private static final String TEST_ETH_IFNAME = "test_eth0";
+    private static final String TEST_VIRT_IFNAME = "test_virt0";
     private static final String TEST_BT_IFNAME = "test_pan0";
     private static final String TETHERING_NAME = "Tethering";
     private static final String[] PROVISIONING_APP_NAME = {"some", "app"};
@@ -265,8 +266,9 @@
     private static final String TEST_P2P_REGEX = "test_p2p-p2p\\d-.*";
     private static final String TEST_BT_REGEX = "test_pan\\d";
     private static final int TEST_CALLER_UID = 1000;
+    private static final int TEST_CALLER_UID_2 = 2000;
     private static final String TEST_CALLER_PKG = "com.test.tethering";
-
+    private static final String TEST_CALLER_PKG_2 = "com.test.tethering2";
     private static final int CELLULAR_NETID = 100;
     private static final int WIFI_NETID = 101;
     private static final int DUN_NETID = 102;
@@ -417,11 +419,12 @@
                             || ifName.equals(TEST_P2P_IFNAME)
                             || ifName.equals(TEST_NCM_IFNAME)
                             || ifName.equals(TEST_ETH_IFNAME)
-                            || ifName.equals(TEST_BT_IFNAME));
+                            || ifName.equals(TEST_BT_IFNAME)
+                            || ifName.equals(TEST_VIRT_IFNAME));
             final String[] ifaces = new String[] {
                     TEST_RNDIS_IFNAME, TEST_WLAN_IFNAME, TEST_WLAN2_IFNAME, TEST_WIFI_IFNAME,
                     TEST_MOBILE_IFNAME, TEST_DUN_IFNAME, TEST_P2P_IFNAME, TEST_NCM_IFNAME,
-                    TEST_ETH_IFNAME};
+                    TEST_ETH_IFNAME, TEST_VIRT_IFNAME};
             return new InterfaceParams(ifName,
                     CollectionUtils.indexOf(ifaces, ifName) + IFINDEX_OFFSET,
                     MacAddress.ALL_ZEROS_ADDRESS);
@@ -674,7 +677,8 @@
         when(mNetd.interfaceGetList())
                 .thenReturn(new String[] {
                         TEST_MOBILE_IFNAME, TEST_WLAN_IFNAME, TEST_WLAN2_IFNAME, TEST_RNDIS_IFNAME,
-                        TEST_P2P_IFNAME, TEST_NCM_IFNAME, TEST_ETH_IFNAME, TEST_BT_IFNAME});
+                        TEST_P2P_IFNAME, TEST_NCM_IFNAME, TEST_ETH_IFNAME, TEST_BT_IFNAME,
+                        TEST_VIRT_IFNAME});
         when(mResources.getString(R.string.config_wifi_tether_enable)).thenReturn("");
         mInterfaceConfiguration = new InterfaceConfigurationParcel();
         mInterfaceConfiguration.flags = new String[0];
@@ -765,12 +769,12 @@
     }
 
     private TetheringRequest createTetheringRequest(final int type) {
-        return createTetheringRequest(type, null, null, false, CONNECTIVITY_SCOPE_GLOBAL);
+        return createTetheringRequest(type, null, null, false, CONNECTIVITY_SCOPE_GLOBAL, null);
     }
 
     private TetheringRequest createTetheringRequest(final int type,
             final LinkAddress localIPv4Address, final LinkAddress staticClientAddress,
-            final boolean exempt, final int scope) {
+            final boolean exempt, final int scope, final String interfaceName) {
         TetheringRequest.Builder builder = new TetheringRequest.Builder(type)
                 .setExemptFromEntitlementCheck(exempt)
                 .setConnectivityScope(scope)
@@ -778,7 +782,13 @@
         if (localIPv4Address != null && staticClientAddress != null) {
             builder.setStaticIpv4Addresses(localIPv4Address, staticClientAddress);
         }
-        return builder.build();
+        if (interfaceName != null) {
+            builder.setInterfaceName(interfaceName);
+        }
+        TetheringRequest request = builder.build();
+        request.setUid(TEST_CALLER_UID);
+        request.setPackageName(TEST_CALLER_PKG);
+        return request;
     }
 
     @NonNull
@@ -922,11 +932,18 @@
         // it creates a IpServer and sends out a broadcast indicating that the
         // interface is "available".
         if (emulateInterfaceStatusChanged) {
-            // There is 1 IpServer state change event: STATE_AVAILABLE
-            verify(mNotificationUpdater, times(1)).onDownstreamChanged(DOWNSTREAM_NONE);
-            verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
-            verify(mWifiManager).updateInterfaceIpState(
-                    TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+            if (!SdkLevel.isAtLeastB()) {
+                // There is 1 IpServer state change event: STATE_AVAILABLE
+                verify(mNotificationUpdater, times(1)).onDownstreamChanged(DOWNSTREAM_NONE);
+                verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
+                verify(mWifiManager).updateInterfaceIpState(
+                        TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+            } else {
+                // Starting in B, ignore the interfaceStatusChanged
+                verify(mNotificationUpdater, never()).onDownstreamChanged(DOWNSTREAM_NONE);
+                verify(mWifiManager, never()).updateInterfaceIpState(
+                        TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+            }
         }
         verifyNoMoreInteractions(mNetd);
         verifyNoMoreInteractions(mWifiManager);
@@ -946,8 +963,8 @@
         mTethering.startTethering(request, TEST_CALLER_PKG, null);
         mLooper.dispatchAll();
 
-        assertEquals(1, mTethering.getActiveTetheringRequests().size());
-        assertEquals(request, mTethering.getActiveTetheringRequests().get(TETHERING_USB));
+        assertEquals(1, mTethering.getPendingTetheringRequests().size());
+        assertEquals(request, mTethering.getPendingTetheringRequests().get(TETHERING_USB));
 
         if (mTethering.getTetheringConfiguration().isUsingNcm()) {
             verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_NCM);
@@ -1025,7 +1042,7 @@
         verify(mWifiManager).updateInterfaceIpState(TEST_WLAN_IFNAME, expectedState);
         verifyNoMoreInteractions(mWifiManager);
 
-        verify(mUpstreamNetworkMonitor).startObserveAllNetworks();
+        verify(mUpstreamNetworkMonitor).startObserveUpstreamNetworks();
         if (isLocalOnly) {
             // There are 2 IpServer state change events: STATE_AVAILABLE -> STATE_LOCAL_ONLY.
             verify(mNotificationUpdater, times(2)).onDownstreamChanged(DOWNSTREAM_NONE);
@@ -1253,7 +1270,7 @@
         // Start USB tethering with no current upstream.
         prepareUsbTethering();
         sendUsbBroadcast(true, true, TETHER_USB_RNDIS_FUNCTION);
-        inOrder.verify(mUpstreamNetworkMonitor).startObserveAllNetworks();
+        inOrder.verify(mUpstreamNetworkMonitor).startObserveUpstreamNetworks();
         inOrder.verify(mUpstreamNetworkMonitor).setTryCell(true);
 
         // Pretend cellular connected and expect the upstream to be set.
@@ -1852,7 +1869,7 @@
         // Start USB tethering with no current upstream.
         prepareUsbTethering();
         sendUsbBroadcast(true, true, TETHER_USB_RNDIS_FUNCTION);
-        inOrder.verify(mUpstreamNetworkMonitor).startObserveAllNetworks();
+        inOrder.verify(mUpstreamNetworkMonitor).startObserveUpstreamNetworks();
         inOrder.verify(mUpstreamNetworkMonitor).setTryCell(true);
         ArgumentCaptor<NetworkCallback> captor = ArgumentCaptor.forClass(NetworkCallback.class);
         inOrder.verify(mCm).requestNetwork(any(), eq(0), eq(TYPE_MOBILE_DUN), any(),
@@ -1923,7 +1940,6 @@
         workingLocalOnlyHotspotEnrichedApBroadcast(false);
     }
 
-    // TODO: Test with and without interfaceStatusChanged().
     @Test
     public void failingWifiTetheringLegacyApBroadcast() throws Exception {
         initTetheringOnTestThread();
@@ -1942,12 +1958,20 @@
         // tethering mode is to be started.
         mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
         sendWifiApStateChanged(WIFI_AP_STATE_ENABLED);
+        mLooper.dispatchAll();
 
-        // There is 1 IpServer state change event: STATE_AVAILABLE
-        verify(mNotificationUpdater, times(1)).onDownstreamChanged(DOWNSTREAM_NONE);
-        verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
-        verify(mWifiManager).updateInterfaceIpState(
-                TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+        if (!SdkLevel.isAtLeastB()) {
+            // There is 1 IpServer state change event: STATE_AVAILABLE from interfaceStatusChanged
+            verify(mNotificationUpdater, times(1)).onDownstreamChanged(DOWNSTREAM_NONE);
+            verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
+            verify(mWifiManager).updateInterfaceIpState(
+                    TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+        } else {
+            // Starting in B, ignore the interfaceStatusChanged
+            verify(mNotificationUpdater, never()).onDownstreamChanged(DOWNSTREAM_NONE);
+            verify(mWifiManager, never()).updateInterfaceIpState(
+                    TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+        }
         verifyNoMoreInteractions(mNetd);
         verifyNoMoreInteractions(mWifiManager);
     }
@@ -2087,7 +2111,7 @@
 
         verify(mNotificationUpdater, times(expectedInteractionsWithShowNotification))
                 .notifyTetheringDisabledByRestriction();
-        verify(mockTethering, times(expectedInteractionsWithShowNotification)).untetherAll();
+        verify(mockTethering, times(expectedInteractionsWithShowNotification)).stopAllTethering();
     }
 
     @Test
@@ -2159,7 +2183,7 @@
         runUsbTethering(upstreamState);
         assertContains(Arrays.asList(mTethering.getTetheredIfaces()), TEST_RNDIS_IFNAME);
         assertTrue(mTethering.isTetheringActive());
-        assertEquals(0, mTethering.getActiveTetheringRequests().size());
+        assertEquals(0, mTethering.getPendingTetheringRequests().size());
 
         final Tethering.UserRestrictionActionListener ural = makeUserRestrictionActionListener(
                 mTethering, false /* currentDisallow */, true /* nextDisallow */);
@@ -2350,14 +2374,19 @@
         UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
         initTetheringUpstream(upstreamState);
         when(mWifiManager.startTetheredHotspot(null)).thenReturn(true);
-        mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
-        mLooper.dispatchAll();
-        tetherState = callback.pollTetherStatesChanged();
-        assertArrayEquals(tetherState.availableList, new TetheringInterface[] {wifiIface});
 
         mTethering.startTethering(createTetheringRequest(TETHERING_WIFI), TEST_CALLER_PKG,
                 null);
+        mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+        mLooper.dispatchAll();
+        if (SdkLevel.isAtLeastB()) {
+            // Starting in B, ignore the interfaceStatusChanged
+            callback.assertNoStateChangeCallback();
+        }
         sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+        mLooper.dispatchAll();
+        tetherState = callback.pollTetherStatesChanged();
+        assertArrayEquals(tetherState.availableList, new TetheringInterface[] {wifiIface});
         tetherState = callback.pollTetherStatesChanged();
         assertArrayEquals(tetherState.tetheredList, new TetheringInterface[] {wifiIface});
         callback.expectUpstreamChanged(upstreamState.network);
@@ -2448,19 +2477,25 @@
         UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
         initTetheringUpstream(upstreamState);
         when(mWifiManager.startTetheredHotspot(null)).thenReturn(true);
+
+        // Enable wifi tethering
+        mBinderCallingUid = TEST_CALLER_UID;
+        mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, null);
         mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
         mLooper.dispatchAll();
+        if (SdkLevel.isAtLeastB()) {
+            // Starting in B, ignore the interfaceStatusChanged
+            callback.assertNoStateChangeCallback();
+        }
+        sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+        mLooper.dispatchAll();
+        // Verify we see  Available -> Tethered states
         assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
                 callback.pollTetherStatesChanged().availableList);
         assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
                 differentCallback.pollTetherStatesChanged().availableList);
         assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
                 settingsCallback.pollTetherStatesChanged().availableList);
-
-        // Enable wifi tethering
-        mBinderCallingUid = TEST_CALLER_UID;
-        mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, null);
-        sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
         assertArrayEquals(new TetheringInterface[] {wifiIfaceWithConfig},
                 callback.pollTetherStatesChanged().tetheredList);
         assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
@@ -2580,7 +2615,7 @@
         verify(mNetd, times(1)).tetherStartWithConfiguration(any());
         verifyNoMoreInteractions(mNetd);
         verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_ACTIVE_LOCAL_ONLY);
-        verify(mUpstreamNetworkMonitor, times(1)).startObserveAllNetworks();
+        verify(mUpstreamNetworkMonitor, times(1)).startObserveUpstreamNetworks();
         // There are 2 IpServer state change events: STATE_AVAILABLE -> STATE_LOCAL_ONLY
         verify(mNotificationUpdater, times(2)).onDownstreamChanged(DOWNSTREAM_NONE);
 
@@ -2751,6 +2786,10 @@
         public void assertHasResult() {
             if (!mHasResult) fail("No callback result");
         }
+
+        public void assertDoesNotHaveResult() {
+            if (mHasResult) fail("Has callback result");
+        }
     }
 
     @Test
@@ -2779,10 +2818,21 @@
         verify(mUsbManager, never()).setCurrentFunctions(UsbManager.FUNCTION_NONE);
         reset(mUsbManager);
 
+        // Enable USB tethering again with the same request but different uid/package and expect no
+        // change to USB.
+        TetheringRequest differentUidPackage = createTetheringRequest(TETHERING_USB);
+        differentUidPackage.setUid(TEST_CALLER_UID_2);
+        differentUidPackage.setPackageName(TEST_CALLER_PKG_2);
+        mTethering.startTethering(differentUidPackage, TEST_CALLER_PKG_2, secondResult);
+        mLooper.dispatchAll();
+        secondResult.assertHasResult();
+        verify(mUsbManager, never()).setCurrentFunctions(UsbManager.FUNCTION_NONE);
+        reset(mUsbManager);
+
         // Enable USB tethering with a different request and expect that USB is stopped and
         // started.
         mTethering.startTethering(createTetheringRequest(TETHERING_USB,
-                  serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL),
+                  serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL, null),
                   TEST_CALLER_PKG, thirdResult);
         mLooper.dispatchAll();
         thirdResult.assertHasResult();
@@ -2813,7 +2863,7 @@
         final ArgumentCaptor<DhcpServingParamsParcel> dhcpParamsCaptor =
                 ArgumentCaptor.forClass(DhcpServingParamsParcel.class);
         mTethering.startTethering(createTetheringRequest(TETHERING_USB,
-                  serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL),
+                  serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL, null),
                   TEST_CALLER_PKG, null);
         mLooper.dispatchAll();
         verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_NCM);
@@ -2942,7 +2992,7 @@
         setupForRequiredProvisioning();
         final TetheringRequest wifiNotExemptRequest =
                 createTetheringRequest(TETHERING_WIFI, null, null, false,
-                        CONNECTIVITY_SCOPE_GLOBAL);
+                        CONNECTIVITY_SCOPE_GLOBAL, null);
         mTethering.startTethering(wifiNotExemptRequest, TEST_CALLER_PKG, null);
         mLooper.dispatchAll();
         verify(mEntitleMgr).startProvisioningIfNeeded(TETHERING_WIFI, false);
@@ -2956,7 +3006,7 @@
         setupForRequiredProvisioning();
         final TetheringRequest wifiExemptRequest =
                 createTetheringRequest(TETHERING_WIFI, null, null, true,
-                        CONNECTIVITY_SCOPE_GLOBAL);
+                        CONNECTIVITY_SCOPE_GLOBAL, null);
         mTethering.startTethering(wifiExemptRequest, TEST_CALLER_PKG, null);
         mLooper.dispatchAll();
         verify(mEntitleMgr, never()).startProvisioningIfNeeded(TETHERING_WIFI, false);
@@ -3348,10 +3398,9 @@
     }
 
     @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
     public void testBluetoothTethering() throws Exception {
         initTetheringOnTestThread();
-        // Switch to @IgnoreUpTo(Build.VERSION_CODES.S_V2) when it is available for AOSP.
-        assumeTrue(isAtLeastT());
 
         final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
         mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
@@ -3385,10 +3434,9 @@
     }
 
     @Test
+    @IgnoreAfter(Build.VERSION_CODES.S_V2)
     public void testBluetoothTetheringBeforeT() throws Exception {
         initTetheringOnTestThread();
-        // Switch to @IgnoreAfter(Build.VERSION_CODES.S_V2) when it is available for AOSP.
-        assumeFalse(isAtLeastT());
 
         final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
         mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
@@ -3404,7 +3452,7 @@
         mTethering.interfaceStatusChanged(TEST_BT_IFNAME, false);
         mTethering.interfaceStatusChanged(TEST_BT_IFNAME, true);
         final ResultListener tetherResult = new ResultListener(TETHER_ERROR_NO_ERROR);
-        mTethering.tether(TEST_BT_IFNAME, IpServer.STATE_TETHERED, tetherResult);
+        mTethering.legacyTether(TEST_BT_IFNAME, tetherResult);
         mLooper.dispatchAll();
         tetherResult.assertHasResult();
 
@@ -3424,7 +3472,7 @@
         mTethering.stopTethering(TETHERING_BLUETOOTH);
         mLooper.dispatchAll();
         final ResultListener untetherResult = new ResultListener(TETHER_ERROR_NO_ERROR);
-        mTethering.untether(TEST_BT_IFNAME, untetherResult);
+        mTethering.legacyUntether(TEST_BT_IFNAME, untetherResult);
         mLooper.dispatchAll();
         untetherResult.assertHasResult();
         verifySetBluetoothTethering(false /* enable */, false /* bindToPanService */);
@@ -3454,7 +3502,7 @@
             mTethering.interfaceStatusChanged(TEST_BT_IFNAME, false);
             mTethering.interfaceStatusChanged(TEST_BT_IFNAME, true);
             final ResultListener tetherResult = new ResultListener(TETHER_ERROR_NO_ERROR);
-            mTethering.tether(TEST_BT_IFNAME, IpServer.STATE_TETHERED, tetherResult);
+            mTethering.legacyTether(TEST_BT_IFNAME, tetherResult);
             mLooper.dispatchAll();
             tetherResult.assertHasResult();
         }
@@ -3468,6 +3516,23 @@
         verifyNetdCommandForBtTearDown();
     }
 
+    @Test
+    public void testPendingPanEnableRequestFailedUponDisableRequest() throws Exception {
+        initTetheringOnTestThread();
+
+        mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
+        final ResultListener failedEnable = new ResultListener(TETHER_ERROR_SERVICE_UNAVAIL);
+        mTethering.startTethering(createTetheringRequest(TETHERING_BLUETOOTH),
+                TEST_CALLER_PKG, failedEnable);
+        mLooper.dispatchAll();
+        failedEnable.assertDoesNotHaveResult();
+
+        // Stop tethering before the pan service connects. This should fail the enable request.
+        mTethering.stopTethering(TETHERING_BLUETOOTH);
+        mLooper.dispatchAll();
+        failedEnable.assertHasResult();
+    }
+
     private void mockBluetoothSettings(boolean bluetoothOn, boolean tetheringOn) {
         when(mBluetoothAdapter.isEnabled()).thenReturn(bluetoothOn);
         when(mBluetoothPan.isTetheringOn()).thenReturn(tetheringOn);
@@ -3750,7 +3815,7 @@
         verifyInterfaceServingModeStarted(TEST_P2P_IFNAME);
         verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_AVAILABLE_TETHER);
         verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_ACTIVE_LOCAL_ONLY);
-        verify(mUpstreamNetworkMonitor).startObserveAllNetworks();
+        verify(mUpstreamNetworkMonitor).startObserveUpstreamNetworks();
         // Verify never enable upstream if only P2P active.
         verify(mUpstreamNetworkMonitor, never()).setTryCell(true);
         assertEquals(TETHER_ERROR_NO_ERROR, mTethering.getLastErrorForTest(TEST_P2P_IFNAME));
@@ -3779,4 +3844,18 @@
 
     // TODO: Test that a request for hotspot mode doesn't interfere with an
     // already operating tethering mode interface.
+
+    @Test
+    public void testVirtualTetheringWithInterfaceName() throws Exception {
+        initTetheringOnTestThread();
+        final TetheringRequest virtualTetheringRequest =
+                createTetheringRequest(TETHERING_VIRTUAL, null, null, false,
+                        CONNECTIVITY_SCOPE_GLOBAL, TEST_VIRT_IFNAME);
+        assertEquals(TEST_VIRT_IFNAME, virtualTetheringRequest.getInterfaceName());
+        mTethering.startTethering(virtualTetheringRequest, TEST_CALLER_PKG, null);
+        mLooper.dispatchAll();
+        assertContains(Arrays.asList(mTethering.getTetheredIfaces()), TEST_VIRT_IFNAME);
+        mTethering.stopTethering(TETHERING_VIRTUAL);
+        mLooper.dispatchAll();
+    }
 }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
index 90fd709..f192492 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
@@ -36,7 +36,6 @@
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.anyString;
 import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -141,7 +140,7 @@
         assertTrue(mCM.hasNoCallbacks());
         mUNM.startTrackDefaultNetwork(mEntitleMgr);
 
-        mUNM.startObserveAllNetworks();
+        mUNM.startObserveUpstreamNetworks();
         assertEquals(1, mCM.mTrackingDefault.size());
 
         mUNM.stop();
@@ -149,13 +148,13 @@
     }
 
     @Test
-    public void testListensForAllNetworks() throws Exception {
+    public void testListensForUpstreamNetworks() throws Exception {
         assertTrue(mCM.mListening.isEmpty());
 
         mUNM.startTrackDefaultNetwork(mEntitleMgr);
-        mUNM.startObserveAllNetworks();
+        mUNM.startObserveUpstreamNetworks();
         assertFalse(mCM.mListening.isEmpty());
-        assertTrue(mCM.isListeningForAll());
+        assertTrue(mCM.isListeningForUpstream());
 
         mUNM.stop();
         assertTrue(mCM.onlyHasDefaultCallbacks());
@@ -179,7 +178,7 @@
             assertTrue(TestConnectivityManager.looksLikeDefaultRequest(requestCaptor.getValue()));
         }
 
-        mUNM.startObserveAllNetworks();
+        mUNM.startObserveUpstreamNetworks();
         verify(mCM, times(1)).registerNetworkCallback(
                 any(NetworkRequest.class), any(NetworkCallback.class), any(Handler.class));
 
@@ -192,7 +191,7 @@
         assertFalse(mUNM.mobileNetworkRequested());
         assertEquals(0, mCM.mRequested.size());
 
-        mUNM.startObserveAllNetworks();
+        mUNM.startObserveUpstreamNetworks();
         assertFalse(mUNM.mobileNetworkRequested());
         assertEquals(0, mCM.mRequested.size());
 
@@ -215,7 +214,7 @@
         assertFalse(mUNM.mobileNetworkRequested());
         assertEquals(0, mCM.mRequested.size());
 
-        mUNM.startObserveAllNetworks();
+        mUNM.startObserveUpstreamNetworks();
         verify(mCM, times(1)).registerNetworkCallback(
                 any(NetworkRequest.class), any(NetworkCallback.class), any(Handler.class));
         assertFalse(mUNM.mobileNetworkRequested());
@@ -251,7 +250,7 @@
         assertFalse(mUNM.mobileNetworkRequested());
         assertEquals(0, mCM.mRequested.size());
 
-        mUNM.startObserveAllNetworks();
+        mUNM.startObserveUpstreamNetworks();
         assertFalse(mUNM.mobileNetworkRequested());
         assertEquals(0, mCM.mRequested.size());
 
@@ -271,7 +270,7 @@
 
     @Test
     public void testUpdateMobileRequiresDun() throws Exception {
-        mUNM.startObserveAllNetworks();
+        mUNM.startObserveUpstreamNetworks();
 
         // Test going from no-DUN to DUN correctly re-registers callbacks.
         mUNM.setUpstreamConfig(false /* autoUpstream */, false /* dunRequired */);
@@ -301,7 +300,7 @@
         preferredTypes.add(TYPE_WIFI);
 
         mUNM.startTrackDefaultNetwork(mEntitleMgr);
-        mUNM.startObserveAllNetworks();
+        mUNM.startObserveUpstreamNetworks();
         // There are no networks, so there is nothing to select.
         assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes));
 
@@ -374,7 +373,7 @@
     @Test
     public void testGetCurrentPreferredUpstream() throws Exception {
         mUNM.startTrackDefaultNetwork(mEntitleMgr);
-        mUNM.startObserveAllNetworks();
+        mUNM.startObserveUpstreamNetworks();
         mUNM.setUpstreamConfig(true /* autoUpstream */, false /* dunRequired */);
         mUNM.setTryCell(true);
 
@@ -446,7 +445,7 @@
     @Test
     public void testLocalPrefixes() throws Exception {
         mUNM.startTrackDefaultNetwork(mEntitleMgr);
-        mUNM.startObserveAllNetworks();
+        mUNM.startObserveUpstreamNetworks();
 
         // [0] Test minimum set of local prefixes.
         Set<IpPrefix> local = mUNM.getLocalPrefixes();
@@ -558,7 +557,7 @@
         preferredTypes.add(TYPE_MOBILE_HIPRI);
         preferredTypes.add(TYPE_WIFI);
         mUNM.startTrackDefaultNetwork(mEntitleMgr);
-        mUNM.startObserveAllNetworks();
+        mUNM.startObserveUpstreamNetworks();
         // Setup wifi and make wifi as default network.
         final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, WIFI_CAPABILITIES);
         wifiAgent.fakeConnect();
@@ -579,7 +578,7 @@
         final String ipv6Addr1 = "2001:db8:4:fd00:827a:bfff:fe6f:374d/64";
         final String ipv6Addr2 = "2003:aa8:3::123/64";
         mUNM.startTrackDefaultNetwork(mEntitleMgr);
-        mUNM.startObserveAllNetworks();
+        mUNM.startObserveUpstreamNetworks();
         mUNM.setUpstreamConfig(true /* autoUpstream */, false /* dunRequired */);
         mUNM.setTryCell(true);
 
diff --git a/bpf/headers/BpfMapTest.cpp b/bpf/headers/BpfMapTest.cpp
index 862114d..33b88fa 100644
--- a/bpf/headers/BpfMapTest.cpp
+++ b/bpf/headers/BpfMapTest.cpp
@@ -250,5 +250,10 @@
     expectMapEmpty(testMap);
 }
 
+TEST_F(BpfMapTest, testGTSbitmapTestOpen) {
+    BpfMap<int, uint64_t> bitmap;
+    ASSERT_RESULT_OK(bitmap.init("/sys/fs/bpf/tethering/map_test_bitmap"));
+}
+
 }  // namespace bpf
 }  // namespace android
diff --git a/bpf/headers/include/bpf/BpfClassic.h b/bpf/headers/include/bpf/BpfClassic.h
index e6cef89..26d8ad5 100644
--- a/bpf/headers/include/bpf/BpfClassic.h
+++ b/bpf/headers/include/bpf/BpfClassic.h
@@ -170,6 +170,9 @@
 // IPv6 extension headers (HOPOPTS, DSTOPS, FRAG) begin with a u8 nexthdr
 #define BPF_LOAD_NETX_RELATIVE_V6EXTHDR_NEXTHDR BPF_LOAD_NETX_RELATIVE_L4_U8(0)
 
+// IPv6 MLD start with u8 type
+#define BPF_LOAD_NETX_RELATIVE_MLD_TYPE BPF_LOAD_NETX_RELATIVE_L4_U8(0)
+
 // IPv6 fragment header is always exactly 8 bytes long
 #define BPF_LOAD_CONSTANT_V6FRAGHDR_LEN \
     BPF_STMT(BPF_LD | BPF_IMM, 8)
diff --git a/bpf/headers/include/bpf/BpfMap.h b/bpf/headers/include/bpf/BpfMap.h
index 1037beb..576cca6 100644
--- a/bpf/headers/include/bpf/BpfMap.h
+++ b/bpf/headers/include/bpf/BpfMap.h
@@ -26,6 +26,7 @@
 #include "BpfSyscallWrappers.h"
 #include "bpf/BpfUtils.h"
 
+#include <cstdio>
 #include <functional>
 
 namespace android {
@@ -35,6 +36,30 @@
 using base::unique_fd;
 using std::function;
 
+#ifdef BPF_MAP_MAKE_VISIBLE_FOR_TESTING
+#undef BPFMAP_VERBOSE_ABORT
+#define BPFMAP_VERBOSE_ABORT
+#endif
+
+[[noreturn]] __attribute__((__format__(__printf__, 2, 3))) static inline
+void Abort(int __unused error, const char* __unused fmt, ...) {
+#ifdef BPFMAP_VERBOSE_ABORT
+    va_list va;
+    va_start(va, fmt);
+
+    fflush(stdout);
+    vfprintf(stderr, fmt, va);
+    if (error) fprintf(stderr, "; errno=%d [%s]", error, strerror(error));
+    putc('\n', stderr);
+    fflush(stderr);
+
+    va_end(va);
+#endif
+
+    abort();
+}
+
+
 // This is a class wrapper for eBPF maps. The eBPF map is a special in-kernel
 // data structure that stores data in <Key, Value> pairs. It can be read/write
 // from userspace by passing syscalls with the map file descriptor. This class
@@ -60,14 +85,21 @@
 
   protected:
     void abortOnMismatch(bool writable) const {
-        if (!mMapFd.ok()) abort();
+        if (!mMapFd.ok()) Abort(errno, "mMapFd %d is not valid", mMapFd.get());
         if (isAtLeastKernelVersion(4, 14, 0)) {
             int flags = bpfGetFdMapFlags(mMapFd);
-            if (flags < 0) abort();
-            if (flags & BPF_F_WRONLY) abort();
-            if (writable && (flags & BPF_F_RDONLY)) abort();
-            if (bpfGetFdKeySize(mMapFd) != sizeof(Key)) abort();
-            if (bpfGetFdValueSize(mMapFd) != sizeof(Value)) abort();
+            if (flags < 0) Abort(errno, "bpfGetFdMapFlags fail: flags=%d", flags);
+            if (flags & BPF_F_WRONLY) Abort(0, "map is write-only (flags=0x%X)", flags);
+            if (writable && (flags & BPF_F_RDONLY))
+                Abort(0, "writable map is actually read-only (flags=0x%X)", flags);
+            int keySize = bpfGetFdKeySize(mMapFd);
+            if (keySize != sizeof(Key))
+                Abort(errno, "map key size mismatch (expected=%zu, actual=%d)",
+                      sizeof(Key), keySize);
+            int valueSize = bpfGetFdValueSize(mMapFd);
+            if (valueSize != sizeof(Value))
+                Abort(errno, "map value size mismatch (expected=%zu, actual=%d)",
+                      sizeof(Value), valueSize);
         }
     }
 
@@ -278,8 +310,8 @@
     [[clang::reinitializes]] Result<void> resetMap(bpf_map_type map_type,
                                                    uint32_t max_entries,
                                                    uint32_t map_flags = 0) {
-        if (map_flags & BPF_F_WRONLY) abort();
-        if (map_flags & BPF_F_RDONLY) abort();
+        if (map_flags & BPF_F_WRONLY) Abort(0, "map_flags is write-only");
+        if (map_flags & BPF_F_RDONLY) Abort(0, "map_flags is read-only");
         mMapFd.reset(createMap(map_type, sizeof(Key), sizeof(Value), max_entries,
                                map_flags));
         if (!mMapFd.ok()) return ErrnoErrorf("BpfMap::resetMap() failed");
diff --git a/bpf/headers/include/bpf/BpfUtils.h b/bpf/headers/include/bpf/BpfUtils.h
index 9dd5822..ed08e1a 100644
--- a/bpf/headers/include/bpf/BpfUtils.h
+++ b/bpf/headers/include/bpf/BpfUtils.h
@@ -26,6 +26,7 @@
 #include <sys/socket.h>
 #include <sys/utsname.h>
 
+#include <android-base/properties.h>
 #include <log/log.h>
 
 #include "KernelUtils.h"
@@ -33,6 +34,16 @@
 namespace android {
 namespace bpf {
 
+const bool unreleased = (base::GetProperty("ro.build.version.codename", "REL") != "REL");
+const int api_level = unreleased ? 10000 : android_get_device_api_level();
+const bool isAtLeastR = (api_level >= 30);
+const bool isAtLeastS = (api_level >= 31);
+// Sv2 is 32
+const bool isAtLeastT = (api_level >= 33);
+const bool isAtLeastU = (api_level >= 34);
+const bool isAtLeastV = (api_level >= 35);
+const bool isAtLeast25Q2 = (api_level >= 36);
+
 // See kernel's net/core/sock_diag.c __sock_gen_cookie()
 // the implementation of which guarantees 0 will never be returned,
 // primarily because 0 is used to mean not yet initialized,
@@ -63,9 +74,9 @@
     // 4.9 kernels. The kernel code of socket release on pf_key socket will
     // explicitly call synchronize_rcu() which is exactly what we need.
     //
-    // Linux 4.14/4.19/5.4/5.10/5.15/6.1 (and 6.3-rc5) still have this same behaviour.
+    // Linux 4.14/4.19/5.4/5.10/5.15/6.1/6.6/6.12 (& 6.13) have this behaviour.
     // see net/key/af_key.c: pfkey_release() -> synchronize_rcu()
-    // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/key/af_key.c?h=v6.3-rc5#n185
+    // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/key/af_key.c?h=v6.13#n185
     const int pfSocket = socket(AF_KEY, SOCK_RAW | SOCK_CLOEXEC, PF_KEY_V2);
 
     if (pfSocket < 0) {
diff --git a/bpf/headers/include/bpf/KernelUtils.h b/bpf/headers/include/bpf/KernelUtils.h
index 68bc607..a36085a 100644
--- a/bpf/headers/include/bpf/KernelUtils.h
+++ b/bpf/headers/include/bpf/KernelUtils.h
@@ -55,12 +55,12 @@
            isKernelVersion(4,  9) ||  // minimum for Android S & T
            isKernelVersion(4, 14) ||  // minimum for Android U
            isKernelVersion(4, 19) ||  // minimum for Android V
-           isKernelVersion(5,  4) ||  // first supported in Android R, min for W
+           isKernelVersion(5,  4) ||  // first supported in Android R, min for 25Q2
            isKernelVersion(5, 10) ||  // first supported in Android S
            isKernelVersion(5, 15) ||  // first supported in Android T
            isKernelVersion(6,  1) ||  // first supported in Android U
            isKernelVersion(6,  6) ||  // first supported in Android V
-           isKernelVersion(6, 12);    // first supported in Android W
+           isKernelVersion(6, 12);    // first supported in Android 25Q2
 }
 
 // Figure out the bitness of userspace.
diff --git a/bpf/headers/include/bpf_helpers.h b/bpf/headers/include/bpf_helpers.h
index b994a9f..6a0e5a8 100644
--- a/bpf/headers/include/bpf_helpers.h
+++ b/bpf/headers/include/bpf_helpers.h
@@ -62,8 +62,8 @@
 // Android Mainline BpfLoader when running on Android V (sdk=35)
 #define BPFLOADER_MAINLINE_V_VERSION (BPFLOADER_MAINLINE_U_QPR3_VERSION + 1u)
 
-// Android Mainline BpfLoader when running on Android W (sdk=36)
-#define BPFLOADER_MAINLINE_W_VERSION (BPFLOADER_MAINLINE_V_VERSION + 1u)
+// Android Mainline BpfLoader when running on Android 25Q2 (sdk=36)
+#define BPFLOADER_MAINLINE_25Q2_VERSION (BPFLOADER_MAINLINE_V_VERSION + 1u)
 
 /* For mainline module use, you can #define BPFLOADER_{MIN/MAX}_VER
  * before #include "bpf_helpers.h" to change which bpfloaders will
@@ -122,28 +122,46 @@
  */
 #define CRITICAL(REASON) char _critical[] SECTION("critical") = (REASON)
 
-/*
- * Helper functions called from eBPF programs written in C. These are
- * implemented in the kernel sources.
- */
+// Helpers for writing kernel version specific bpf programs
 
 struct kver_uint { unsigned int kver; };
 #define KVER_(v) ((struct kver_uint){ .kver = (v) })
 #define KVER(a, b, c) KVER_(((a) << 24) + ((b) << 16) + (c))
 #define KVER_NONE KVER_(0)
+#define KVER_4_9  KVER(4, 9, 0)
 #define KVER_4_14 KVER(4, 14, 0)
 #define KVER_4_19 KVER(4, 19, 0)
 #define KVER_5_4  KVER(5, 4, 0)
-#define KVER_5_8  KVER(5, 8, 0)
-#define KVER_5_9  KVER(5, 9, 0)
 #define KVER_5_10 KVER(5, 10, 0)
 #define KVER_5_15 KVER(5, 15, 0)
 #define KVER_6_1  KVER(6, 1, 0)
 #define KVER_6_6  KVER(6, 6, 0)
+#define KVER_6_12 KVER(6, 12, 0)
 #define KVER_INF KVER_(0xFFFFFFFFu)
 
 #define KVER_IS_AT_LEAST(kver, a, b, c) ((kver).kver >= KVER(a, b, c).kver)
 
+// Helpers for writing sdk level specific bpf programs
+//
+// Note: we choose to follow sdk api level values, but there is no real need for this:
+// These just need to be monotonically increasing.  We could also use values ten or even
+// a hundred times larger to leave room for quarters or months.  We may also just use
+// dates or something (2502 or 202506 for 25Q2) or even the mainline bpfloader version...
+// For now this easily suffices for our use case.
+
+struct sdk_level_uint { unsigned int sdk_level; };
+#define SDK_LEVEL_(v) ((struct sdk_level_uint){ .sdk_level = (v) })
+#define SDK_LEVEL_NONE SDK_LEVEL_(0)
+#define SDK_LEVEL_S    SDK_LEVEL_(31) // Android 12
+#define SDK_LEVEL_Sv2  SDK_LEVEL_(32) // Android 12L
+#define SDK_LEVEL_T    SDK_LEVEL_(33) // Android 13
+#define SDK_LEVEL_U    SDK_LEVEL_(34) // Android 14
+#define SDK_LEVEL_V    SDK_LEVEL_(35) // Android 15
+#define SDK_LEVEL_24Q3 SDK_LEVEL_V
+#define SDK_LEVEL_25Q2 SDK_LEVEL_(36) // Android 16
+
+#define SDK_LEVEL_IS_AT_LEAST(lvl, v) ((lvl).sdk_level >= (SDK_LEVEL_##v).sdk_level)
+
 /*
  * BPFFS (ie. /sys/fs/bpf) labelling is as follows:
  *   subdirectory   selinux context      mainline  usecase / usable by
@@ -168,6 +186,11 @@
  * See cs/p:aosp-master%20-file:prebuilts/%20file:genfs_contexts%20"genfscon%20bpf"
  */
 
+/*
+ * Helper functions called from eBPF programs written in C. These are
+ * implemented in the kernel sources.
+ */
+
 /* generic functions */
 
 /*
@@ -231,16 +254,24 @@
               (ignore_userdebug).ignore_on_userdebug),                                   \
         "bpfloader min version must be >= 0.33 in order to use ignored_on");
 
+#define ABSOLUTE(x) ((x) < 0 ? -(x) : (x))
+
+#define DEFAULT_BPF_MAP_FLAGS(type, num_entries, mapflags)    \
+    ( (mapflags) |                                            \
+      ((num_entries) < 0 ? BPF_F_NO_PREALLOC : 0) |           \
+      (type == BPF_MAP_TYPE_LPM_TRIE ? BPF_F_NO_PREALLOC : 0) \
+    )
+
 #define DEFINE_BPF_MAP_BASE(the_map, TYPE, keysize, valuesize, num_entries, \
                             usr, grp, md, selinux, pindir, share, minkver,  \
                             maxkver, minloader, maxloader, ignore_eng,      \
-                            ignore_user, ignore_userdebug)                  \
+                            ignore_user, ignore_userdebug, mapflags)        \
     const struct bpf_map_def SECTION("maps") the_map = {                    \
         .type = BPF_MAP_TYPE_##TYPE,                                        \
         .key_size = (keysize),                                              \
         .value_size = (valuesize),                                          \
-        .max_entries = (num_entries),                                       \
-        .map_flags = 0,                                                     \
+        .max_entries = ABSOLUTE(num_entries),                               \
+        .map_flags = DEFAULT_BPF_MAP_FLAGS(BPF_MAP_TYPE_##TYPE, num_entries, mapflags), \
         .uid = (usr),                                                       \
         .gid = (grp),                                                       \
         .mode = (md),                                                       \
@@ -260,16 +291,17 @@
 // Type safe macro to declare a ring buffer and related output functions.
 // Compatibility:
 // * BPF ring buffers are only available kernels 5.8 and above. Any program
-//   accessing the ring buffer should set a program level min_kver >= 5.8.
-// * The definition below sets a map min_kver of 5.8 which requires targeting
+//   accessing the ring buffer should set a program level min_kver >= 5.10,
+//   since 5.10 is the next LTS version.
+// * The definition below sets a map min_kver of 5.10 which requires targeting
 //   a BPFLOADER_MIN_VER >= BPFLOADER_S_VERSION.
 #define DEFINE_BPF_RINGBUF_EXT(the_map, ValueType, size_bytes, usr, grp, md,   \
                                selinux, pindir, share, min_loader, max_loader, \
                                ignore_eng, ignore_user, ignore_userdebug)      \
     DEFINE_BPF_MAP_BASE(the_map, RINGBUF, 0, 0, size_bytes, usr, grp, md,      \
-                        selinux, pindir, share, KVER_5_8, KVER_INF,            \
+                        selinux, pindir, share, KVER_5_10, KVER_INF,           \
                         min_loader, max_loader, ignore_eng, ignore_user,       \
-                        ignore_userdebug);                                     \
+                        ignore_userdebug, 0);                                  \
                                                                                \
     _Static_assert((size_bytes) >= 4096, "min 4 kiB ringbuffer size");         \
     _Static_assert((size_bytes) <= 0x10000000, "max 256 MiB ringbuffer size"); \
@@ -317,11 +349,11 @@
 /* type safe macro to declare a map and related accessor functions */
 #define DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md,         \
                            selinux, pindir, share, min_loader, max_loader, ignore_eng,           \
-                           ignore_user, ignore_userdebug)                                        \
+                           ignore_user, ignore_userdebug, mapFlags)                              \
   DEFINE_BPF_MAP_BASE(the_map, TYPE, sizeof(KeyType), sizeof(ValueType),                         \
                       num_entries, usr, grp, md, selinux, pindir, share,                         \
                       KVER_NONE, KVER_INF, min_loader, max_loader,                               \
-                      ignore_eng, ignore_user, ignore_userdebug);                                \
+                      ignore_eng, ignore_user, ignore_userdebug, mapFlags);                      \
     BPF_MAP_ASSERT_OK(BPF_MAP_TYPE_##TYPE, (num_entries), (md));                                 \
     _Static_assert(sizeof(KeyType) < 1024, "aosp/2370288 requires < 1024 byte keys");            \
     _Static_assert(sizeof(ValueType) < 65536, "aosp/2370288 requires < 65536 byte values");      \
@@ -359,13 +391,13 @@
 #define DEFINE_BPF_MAP_KERNEL_INTERNAL(the_map, TYPE, KeyType, ValueType, num_entries)           \
     DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, AID_ROOT, AID_ROOT,       \
                        0000, "fs_bpf_loader", "", PRIVATE, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, \
-                       LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+                       LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG, 0)
 
 #define DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md) \
     DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md,     \
                        DEFAULT_BPF_MAP_SELINUX_CONTEXT, DEFAULT_BPF_MAP_PIN_SUBDIR,      \
                        PRIVATE, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER,                    \
-                       LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+                       LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG, 0)
 
 #define DEFINE_BPF_MAP(the_map, TYPE, KeyType, ValueType, num_entries) \
     DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
@@ -387,6 +419,22 @@
     DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
                        DEFAULT_BPF_MAP_UID, gid, 0660)
 
+// idea from Linux include/linux/compiler_types.h (eBPF is always a 64-bit arch)
+#define NATIVE_WORD(t) ((sizeof(t) == 1) || (sizeof(t) == 2) || (sizeof(t) == 4) || (sizeof(t) == 8))
+
+// simplified from Linux include/asm-generic/rwonce.h
+#define READ_ONCE(x) \
+  ({ \
+    _Static_assert(NATIVE_WORD(x), "READ_ONCE requires a native word size"); \
+    (*(const volatile typeof(x) *)&(x)) \
+  })
+
+#define WRITE_ONCE(x, value) \
+  do { \
+    _Static_assert(NATIVE_WORD(x), "WRITE_ONCE requires a native word size"); \
+    *(volatile typeof(x) *)&(x) = (value); \
+  } while (0)
+
 // LLVM eBPF builtins: they directly generate BPF_LD_ABS/BPF_LD_IND (skb may be ignored?)
 unsigned long long load_byte(void* skb, unsigned long long off) asm("llvm.bpf.load.byte");
 unsigned long long load_half(void* skb, unsigned long long off) asm("llvm.bpf.load.half");
diff --git a/bpf/headers/include/bpf_map_def.h b/bpf/headers/include/bpf_map_def.h
index 2d6736c..e95ca5f 100644
--- a/bpf/headers/include/bpf_map_def.h
+++ b/bpf/headers/include/bpf_map_def.h
@@ -94,6 +94,10 @@
 _Static_assert(_Alignof(enum bpf_map_type) == 4, "_Alignof enum bpf_map_type != 4");
 
 // Linux kernel requires sizeof(int) == 4, sizeof(void*) == sizeof(long), sizeof(long long) == 8
+_Static_assert(sizeof(int) == 4, "sizeof int != 4");
+_Static_assert(__alignof__(int) == 4, "__alignof__ int != 4");
+_Static_assert(_Alignof(int) == 4, "_Alignof int != 4");
+
 _Static_assert(sizeof(unsigned int) == 4, "sizeof unsigned int != 4");
 _Static_assert(__alignof__(unsigned int) == 4, "__alignof__ unsigned int != 4");
 _Static_assert(_Alignof(unsigned int) == 4, "_Alignof unsigned int != 4");
@@ -102,8 +106,12 @@
 // Here sizeof & __alignof__ are consistent, but _Alignof is not: compile for 'aosp_cf_x86_phone'
 _Static_assert(sizeof(unsigned long long) == 8, "sizeof unsigned long long != 8");
 _Static_assert(__alignof__(unsigned long long) == 8, "__alignof__ unsigned long long != 8");
-// BPF wants 8, but 32-bit x86 wants 4
-//_Static_assert(_Alignof(unsigned long long) == 8, "_Alignof unsigned long long != 8");
+// BPF & everyone else wants 8, but 32-bit x86 wants 4
+#if defined(__i386__)
+_Static_assert(_Alignof(unsigned long long) == 4, "x86-32 _Alignof unsigned long long != 4");
+#else
+_Static_assert(_Alignof(unsigned long long) == 8, "_Alignof unsigned long long != 8");
+#endif
 
 
 // for maps:
@@ -155,7 +163,7 @@
     enum bpf_map_type type;
     unsigned int key_size;
     unsigned int value_size;
-    unsigned int max_entries;
+    int max_entries;  // negative means BPF_F_NO_PREALLOC, but *might* not work with S
     unsigned int map_flags;
 
     // The following are not supported by the Android bpfloader:
diff --git a/bpf/loader/NetBpfLoad.cpp b/bpf/loader/NetBpfLoad.cpp
index c2a1d6e..9486e75 100644
--- a/bpf/loader/NetBpfLoad.cpp
+++ b/bpf/loader/NetBpfLoad.cpp
@@ -61,6 +61,7 @@
 
 // The following matches bpf_helpers.h, which is only for inclusion in bpf code
 #define BPFLOADER_MAINLINE_VERSION 42u
+#define BPFLOADER_MAINLINE_25Q2_VERSION 47u
 
 using android::base::EndsWith;
 using android::base::GetIntProperty;
@@ -616,9 +617,6 @@
     if (type == BPF_MAP_TYPE_DEVMAP || type == BPF_MAP_TYPE_DEVMAP_HASH)
         desired_map_flags |= BPF_F_RDONLY_PROG;
 
-    if (type == BPF_MAP_TYPE_LPM_TRIE)
-        desired_map_flags |= BPF_F_NO_PREALLOC;
-
     // The .h file enforces that this is a power of two, and page size will
     // also always be a power of two, so this logic is actually enough to
     // force it to be a multiple of the page size, as required by the kernel.
@@ -732,6 +730,12 @@
         }
 
         enum bpf_map_type type = md[i].type;
+        if (type == BPF_MAP_TYPE_LPM_TRIE && !isAtLeastKernelVersion(4, 14, 0)) {
+            // On Linux Kernels older than 4.14 this map type doesn't exist - autoskip.
+            ALOGD("skipping LPM_TRIE map %s - requires kver 4.14+", mapNames[i].c_str());
+            mapFds.push_back(unique_fd());
+            continue;
+        }
         if (type == BPF_MAP_TYPE_DEVMAP && !isAtLeastKernelVersion(4, 14, 0)) {
             // On Linux Kernels older than 4.14 this map type doesn't exist, but it can kind
             // of be approximated: ARRAY has the same userspace api, though it is not usable
@@ -794,7 +798,7 @@
               .key_size = md[i].key_size,
               .value_size = md[i].value_size,
               .max_entries = max_entries,
-              .map_flags = md[i].map_flags | (type == BPF_MAP_TYPE_LPM_TRIE ? BPF_F_NO_PREALLOC : 0),
+              .map_flags = md[i].map_flags,
             };
             if (isAtLeastKernelVersion(4, 15, 0))
                 strlcpy(req.map_name, mapNames[i].c_str(), sizeof(req.map_name));
@@ -820,14 +824,14 @@
                                    "tmp_map_" + objName + "_" + mapNames[i];
                 ret = bpfFdPin(fd, createLoc.c_str());
                 if (ret) {
-                    int err = errno;
+                    const int err = errno;
                     ALOGE("create %s -> %d [%d:%s]", createLoc.c_str(), ret, err, strerror(err));
                     return -err;
                 }
                 ret = renameat2(AT_FDCWD, createLoc.c_str(),
                                 AT_FDCWD, mapPinLoc.c_str(), RENAME_NOREPLACE);
                 if (ret) {
-                    int err = errno;
+                    const int err = errno;
                     ALOGE("rename %s %s -> %d [%d:%s]", createLoc.c_str(), mapPinLoc.c_str(), ret,
                           err, strerror(err));
                     return -err;
@@ -835,32 +839,34 @@
             } else {
                 ret = bpfFdPin(fd, mapPinLoc.c_str());
                 if (ret) {
-                    int err = errno;
+                    const int err = errno;
                     ALOGE("pin %s -> %d [%d:%s]", mapPinLoc.c_str(), ret, err, strerror(err));
                     return -err;
                 }
             }
             ret = chmod(mapPinLoc.c_str(), md[i].mode);
             if (ret) {
-                int err = errno;
+                const int err = errno;
                 ALOGE("chmod(%s, 0%o) = %d [%d:%s]", mapPinLoc.c_str(), md[i].mode, ret, err,
                       strerror(err));
                 return -err;
             }
             ret = chown(mapPinLoc.c_str(), (uid_t)md[i].uid, (gid_t)md[i].gid);
             if (ret) {
-                int err = errno;
+                const int err = errno;
                 ALOGE("chown(%s, %u, %u) = %d [%d:%s]", mapPinLoc.c_str(), md[i].uid, md[i].gid,
                       ret, err, strerror(err));
                 return -err;
             }
         }
 
-        int mapId = bpfGetFdMapId(fd);
-        if (mapId == -1) {
-            if (isAtLeastKernelVersion(4, 14, 0))
-                ALOGE("bpfGetFdMapId failed, ret: %d [%d]", mapId, errno);
-        } else {
+        if (isAtLeastKernelVersion(4, 14, 0)) {
+            int mapId = bpfGetFdMapId(fd);
+            if (mapId == -1) {
+                const int err = errno;
+                ALOGE("bpfGetFdMapId failed, errno: %d", err);
+                return -err;
+            }
             ALOGI("map %s id %d", mapPinLoc.c_str(), mapId);
         }
 
@@ -1003,7 +1009,7 @@
         if (access(progPinLoc.c_str(), F_OK) == 0) {
             fd.reset(retrieveProgram(progPinLoc.c_str()));
             ALOGD("New bpf prog load reusing prog %s, ret: %d (%s)", progPinLoc.c_str(), fd.get(),
-                  (!fd.ok() ? std::strerror(errno) : "no error"));
+                  !fd.ok() ? std::strerror(errno) : "ok");
             reuse = true;
         } else {
             static char log_buf[1 << 20];  // 1 MiB logging buffer
@@ -1034,7 +1040,7 @@
 
             ALOGD("BPF_PROG_LOAD call for %s (%s) returned '%s' fd: %d (%s)", elfPath,
                   cs[i].name.c_str(), log_oneline ? log_buf : "{multiline}",
-                  fd.get(), (!fd.ok() ? std::strerror(errno) : "ok"));
+                  fd.get(), !fd.ok() ? std::strerror(errno) : "ok");
 
             if (!fd.ok()) {
                 // kernel NULL terminates log_buf, so this checks for non-empty string
@@ -1063,14 +1069,14 @@
                                    "tmp_prog_" + objName + '_' + string(name);
                 ret = bpfFdPin(fd, createLoc.c_str());
                 if (ret) {
-                    int err = errno;
+                    const int err = errno;
                     ALOGE("create %s -> %d [%d:%s]", createLoc.c_str(), ret, err, strerror(err));
                     return -err;
                 }
                 ret = renameat2(AT_FDCWD, createLoc.c_str(),
                                 AT_FDCWD, progPinLoc.c_str(), RENAME_NOREPLACE);
                 if (ret) {
-                    int err = errno;
+                    const int err = errno;
                     ALOGE("rename %s %s -> %d [%d:%s]", createLoc.c_str(), progPinLoc.c_str(), ret,
                           err, strerror(err));
                     return -err;
@@ -1078,30 +1084,52 @@
             } else {
                 ret = bpfFdPin(fd, progPinLoc.c_str());
                 if (ret) {
-                    int err = errno;
+                    const int err = errno;
                     ALOGE("create %s -> %d [%d:%s]", progPinLoc.c_str(), ret, err, strerror(err));
                     return -err;
                 }
             }
             if (chmod(progPinLoc.c_str(), 0440)) {
-                int err = errno;
+                const int err = errno;
                 ALOGE("chmod %s 0440 -> [%d:%s]", progPinLoc.c_str(), err, strerror(err));
                 return -err;
             }
             if (chown(progPinLoc.c_str(), (uid_t)cs[i].prog_def->uid,
                       (gid_t)cs[i].prog_def->gid)) {
-                int err = errno;
+                const int err = errno;
                 ALOGE("chown %s %d %d -> [%d:%s]", progPinLoc.c_str(), cs[i].prog_def->uid,
                       cs[i].prog_def->gid, err, strerror(err));
                 return -err;
             }
         }
 
-        int progId = bpfGetFdProgId(fd);
-        if (progId == -1) {
-            ALOGE("bpfGetFdProgId failed, ret: %d [%d]", progId, errno);
-        } else {
-            ALOGI("prog %s id %d", progPinLoc.c_str(), progId);
+        if (isAtLeastKernelVersion(4, 14, 0)) {
+            int progId = bpfGetFdProgId(fd);
+            if (progId == -1) {
+                const int err = errno;
+                ALOGE("bpfGetFdProgId failed, errno: %d", err);
+                return -err;
+            }
+
+            int jitLen = bpfGetFdJitProgLen(fd);
+            if (jitLen == -1) {
+                const int err = errno;
+                ALOGE("bpfGetFdJitProgLen failed, ret: %d", err);
+                return -err;
+            }
+
+            int xlatLen = bpfGetFdXlatProgLen(fd);
+            if (xlatLen == -1) {
+                const int err = errno;
+                ALOGE("bpfGetFdXlatProgLen failed, ret: %d", err);
+                return -err;
+            }
+            ALOGI("prog %s id %d len jit:%d xlat:%d", progPinLoc.c_str(), progId, jitLen, xlatLen);
+
+            if (!jitLen && bpfloader_ver >= BPFLOADER_MAINLINE_25Q2_VERSION) {
+                ALOGE("Kernel eBPF JIT failure for %s", progPinLoc.c_str());
+                return -ENOTSUP;
+            }
         }
     }
 
@@ -1386,40 +1414,7 @@
 static int doLoad(char** argv, char * const envp[]) {
     const bool runningAsRoot = !getuid();  // true iff U QPR3 or V+
 
-    // Any released device will have codename REL instead of a 'real' codename.
-    // For safety: default to 'REL' so we default to unreleased=false on failure.
-    const bool unreleased = (GetProperty("ro.build.version.codename", "REL") != "REL");
-
-    // goog/main device_api_level is bumped *way* before aosp/main api level
-    // (the latter only gets bumped during the push of goog/main to aosp/main)
-    //
-    // Since we develop in AOSP, we want it to behave as if it was bumped too.
-    //
-    // Note that AOSP doesn't really have a good api level (for example during
-    // early V dev cycle, it would have *all* of T, some but not all of U, and some V).
-    // One could argue that for our purposes AOSP api level should be infinite or 10000.
-    //
-    // This could also cause api to be increased in goog/main or other branches,
-    // but I can't imagine a case where this would be a problem: the problem
-    // is rather a too low api level, rather than some ill defined high value.
-    // For example as I write this aosp is 34/U, and goog is 35/V,
-    // we want to treat both goog & aosp as 35/V, but it's harmless if we
-    // treat goog as 36 because that value isn't yet defined to mean anything,
-    // and we thus never compare against it.
-    //
-    // Also note that 'android_get_device_api_level()' is what the
-    //   //system/core/init/apex_init_util.cpp
-    // apex init .XXrc parsing code uses for XX filtering.
-    //
-    // That code has a hack to bump <35 to 35 (to force aosp/main to parse .35rc),
-    // but could (should?) perhaps be adjusted to match this.
-    const int effective_api_level = android_get_device_api_level() + (int)unreleased;
-    const bool isAtLeastT = (effective_api_level >= __ANDROID_API_T__);
-    const bool isAtLeastU = (effective_api_level >= __ANDROID_API_U__);
-    const bool isAtLeastV = (effective_api_level >= __ANDROID_API_V__);
-    const bool isAtLeastW = (effective_api_level >  __ANDROID_API_V__);  // TODO: switch to W
-
-    const int first_api_level = GetIntProperty("ro.board.first_api_level", effective_api_level);
+    const int first_api_level = GetIntProperty("ro.board.first_api_level", api_level);
 
     // last in U QPR2 beta1
     const bool has_platform_bpfloader_rc = exists("/system/etc/init/bpfloader.rc");
@@ -1432,10 +1427,10 @@
     if (isAtLeastU) ++bpfloader_ver;     // [44] BPFLOADER_MAINLINE_U_VERSION
     if (runningAsRoot) ++bpfloader_ver;  // [45] BPFLOADER_MAINLINE_U_QPR3_VERSION
     if (isAtLeastV) ++bpfloader_ver;     // [46] BPFLOADER_MAINLINE_V_VERSION
-    if (isAtLeastW) ++bpfloader_ver;     // [47] BPFLOADER_MAINLINE_W_VERSION
+    if (isAtLeast25Q2) ++bpfloader_ver;  // [47] BPFLOADER_MAINLINE_25Q2_VERSION
 
     ALOGI("NetBpfLoad v0.%u (%s) api:%d/%d kver:%07x (%s) uid:%d rc:%d%d",
-          bpfloader_ver, argv[0], android_get_device_api_level(), effective_api_level,
+          bpfloader_ver, argv[0], android_get_device_api_level(), api_level,
           kernelVersion(), describeArch(), getuid(),
           has_platform_bpfloader_rc, has_platform_netbpfload_rc);
 
@@ -1475,6 +1470,13 @@
         return 1;
     }
 
+    // 25Q2 bumps the kernel requirement up to 5.4
+    // see also: //system/netd/tests/kernel_test.cpp TestKernel54
+    if (isAtLeast25Q2 && !isAtLeastKernelVersion(5, 4, 0)) {
+        ALOGE("Android 25Q2 requires kernel 5.4.");
+        return 1;
+    }
+
     // Technically already required by U, but only enforce on V+
     // see also: //system/netd/tests/kernel_test.cpp TestKernel64Bit
     if (isAtLeastV && isKernel32Bit() && isAtLeastKernelVersion(5, 16, 0)) {
@@ -1482,6 +1484,11 @@
         if (!isTV()) return 1;
     }
 
+    if (isKernel32Bit() && isAtLeast25Q2) {
+        ALOGE("Android 25Q2 requires 64 bit kernel.");
+        return 1;
+    }
+
     // 6.6 is highest version supported by Android V, so this is effectively W+ (sdk=36+)
     if (isKernel32Bit() && isAtLeastKernelVersion(6, 7, 0)) {
         ALOGE("Android platform with 32 bit kernel version >= 6.7.0 is unsupported");
@@ -1498,13 +1505,13 @@
         bool bad = false;
 
         if (!isLtsKernel()) {
-            ALOGW("Android V only supports LTS kernels.");
+            ALOGW("Android V+ only supports LTS kernels.");
             bad = true;
         }
 
 #define REQUIRE(maj, min, sub) \
         if (isKernelVersion(maj, min) && !isAtLeastKernelVersion(maj, min, sub)) { \
-            ALOGW("Android V requires %d.%d kernel to be %d.%d.%d+.", maj, min, maj, min, sub); \
+            ALOGW("Android V+ requires %d.%d kernel to be %d.%d.%d+.", maj, min, maj, min, sub); \
             bad = true; \
         }
 
@@ -1514,6 +1521,7 @@
         REQUIRE(5, 15, 136)
         REQUIRE(6, 1, 57)
         REQUIRE(6, 6, 0)
+        REQUIRE(6, 12, 0)
 
 #undef REQUIRE
 
@@ -1544,16 +1552,15 @@
      *
      * Additionally the 32-bit kernel jit support is poor,
      * and 32-bit userspace on 64-bit kernel bpf ringbuffer compatibility is broken.
+     * Note, however, that TV and Wear devices will continue to support 32-bit userspace
+     * on ARM64.
      */
     if (isUserspace32bit() && isAtLeastKernelVersion(6, 2, 0)) {
         // Stuff won't work reliably, but...
-        if (isTV()) {
-            // exempt TVs... they don't really need functional advanced networking
-            ALOGW("[TV] 32-bit userspace unsupported on 6.2+ kernels.");
-        } else if (isWear() && isArm()) {
-            // exempt Arm Wear devices (arm32 ABI is far less problematic than x86-32)
-            ALOGW("[Arm Wear] 32-bit userspace unsupported on 6.2+ kernels.");
-        } else if (first_api_level <= __ANDROID_API_T__ && isArm()) {
+        if (isArm() && (isTV() || isWear())) {
+            // exempt Arm TV or Wear devices (arm32 ABI is far less problematic than x86-32)
+            ALOGW("[Arm TV/Wear] 32-bit userspace unsupported on 6.2+ kernels.");
+        } else if (first_api_level <= 33 /*T*/ && isArm()) {
             // also exempt Arm devices upgrading with major kernel rev from T-
             // might possibly be better for them to run with a newer kernel...
             ALOGW("[Arm KernelUpRev] 32-bit userspace unsupported on 6.2+ kernels.");
@@ -1566,8 +1573,8 @@
         }
     }
 
-    // Note: 6.6 is highest version supported by Android V (sdk=35), so this is for sdk=36+
-    if (isUserspace32bit() && isAtLeastKernelVersion(6, 7, 0)) {
+    // On handheld, 6.6 is highest version supported by Android V (sdk=35), so this is for sdk=36+
+    if (!isArm() && isUserspace32bit() && isAtLeastKernelVersion(6, 7, 0)) {
         ALOGE("64-bit userspace required on 6.7+ kernels.");
         return 1;
     }
@@ -1659,17 +1666,17 @@
     }
 
     // unreachable before U QPR3
-    {
+    if (exists(uprobestatsBpfLoader)) {
       ALOGI("done, transferring control to uprobestatsbpfload.");
       const char *args[] = {
           uprobestatsBpfLoader,
           NULL,
       };
       execve(args[0], (char **)args, envp);
+      ALOGI("unable to execute uprobestatsbpfload, transferring control to "
+            "platform bpfloader.");
     }
 
-    ALOGI("unable to execute uprobestatsbpfload, transferring control to "
-          "platform bpfloader.");
     // platform BpfLoader *needs* to run as root
     const char * args[] = { platformBpfLoader, NULL, };
     execve(args[0], (char**)args, envp);
diff --git a/bpf/netd/BpfHandler.cpp b/bpf/netd/BpfHandler.cpp
index 340acda..e3e508b 100644
--- a/bpf/netd/BpfHandler.cpp
+++ b/bpf/netd/BpfHandler.cpp
@@ -22,7 +22,6 @@
 #include <inttypes.h>
 
 #include <android-base/unique_fd.h>
-#include <android-modules-utils/sdk_level.h>
 #include <bpf/WaitForProgsLoaded.h>
 #include <log/log.h>
 #include <netdutils/UidConstants.h>
@@ -36,6 +35,12 @@
 using base::unique_fd;
 using base::WaitForProperty;
 using bpf::getSocketCookie;
+using bpf::isAtLeastKernelVersion;
+using bpf::isAtLeastT;
+using bpf::isAtLeastU;
+using bpf::isAtLeastV;
+using bpf::isAtLeast25Q2;
+using bpf::queryProgram;
 using bpf::retrieveProgram;
 using netdutils::Status;
 using netdutils::statusFromErrno;
@@ -56,7 +61,7 @@
     if (!cgroupProg.ok()) {
         return statusFromErrno(errno, fmt::format("Failed to get program from {}", programPath));
     }
-    if (android::bpf::attachProgram(type, cgroupProg, cgroupFd)) {
+    if (bpf::attachProgram(type, cgroupProg, cgroupFd)) {
         return statusFromErrno(errno, fmt::format("Program {} attach failed", programPath));
     }
     return netdutils::status::ok;
@@ -74,30 +79,36 @@
     if (!cg2_path) return Status("cg2_path is NULL");
 
     // This code was mainlined in T, so this should be trivially satisfied.
-    if (!modules::sdklevel::IsAtLeastT()) return Status("S- platform is unsupported");
+    if (!isAtLeastT) return Status("S- platform is unsupported");
 
     // S requires eBPF support which was only added in 4.9, so this should be satisfied.
-    if (!bpf::isAtLeastKernelVersion(4, 9, 0)) {
+    if (!isAtLeastKernelVersion(4, 9, 0)) {
         return Status("kernel version < 4.9.0 is unsupported");
     }
 
     // U bumps the kernel requirement up to 4.14
-    if (modules::sdklevel::IsAtLeastU() && !bpf::isAtLeastKernelVersion(4, 14, 0)) {
+    if (isAtLeastU && !isAtLeastKernelVersion(4, 14, 0)) {
         return Status("U+ platform with kernel version < 4.14.0 is unsupported");
     }
 
     // U mandates this mount point (though it should also be the case on T)
-    if (modules::sdklevel::IsAtLeastU() && !!strcmp(cg2_path, "/sys/fs/cgroup")) {
+    if (isAtLeastU && !!strcmp(cg2_path, "/sys/fs/cgroup")) {
         return Status("U+ platform with cg2_path != /sys/fs/cgroup is unsupported");
     }
 
-    unique_fd cg_fd(open(cg2_path, O_DIRECTORY | O_RDONLY | O_CLOEXEC));
-    if (!cg_fd.ok()) {
-        const int err = errno;
-        ALOGE("Failed to open the cgroup directory: %s", strerror(err));
-        return statusFromErrno(err, "Open the cgroup directory failed");
+    // V bumps the kernel requirement up to 4.19
+    if (isAtLeastV && !isAtLeastKernelVersion(4, 19, 0)) {
+        return Status("V+ platform with kernel version < 4.19.0 is unsupported");
     }
 
+    // 25Q2 bumps the kernel requirement up to 5.4
+    if (isAtLeast25Q2 && !isAtLeastKernelVersion(5, 4, 0)) {
+        return Status("25Q2+ platform with kernel version < 5.4.0 is unsupported");
+    }
+
+    unique_fd cg_fd(open(cg2_path, O_DIRECTORY | O_RDONLY | O_CLOEXEC));
+    if (!cg_fd.ok()) return statusFromErrno(errno, "Opening cgroup dir failed");
+
     RETURN_IF_NOT_OK(checkProgramAccessible(XT_BPF_ALLOWLIST_PROG_PATH));
     RETURN_IF_NOT_OK(checkProgramAccessible(XT_BPF_DENYLIST_PROG_PATH));
     RETURN_IF_NOT_OK(checkProgramAccessible(XT_BPF_EGRESS_PROG_PATH));
@@ -110,20 +121,20 @@
     // cgroup if the program is pinned properly.
     // TODO: delete the if statement once all devices should support cgroup
     // socket filter (ie. the minimum kernel version required is 4.14).
-    if (bpf::isAtLeastKernelVersion(4, 14, 0)) {
+    if (isAtLeastKernelVersion(4, 14, 0)) {
         RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_INET_CREATE_PROG_PATH,
                                     cg_fd, BPF_CGROUP_INET_SOCK_CREATE));
     }
 
-    if (bpf::isAtLeastKernelVersion(5, 10, 0)) {
+    if (isAtLeastKernelVersion(5, 10, 0)) {
         RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_INET_RELEASE_PROG_PATH,
                                     cg_fd, BPF_CGROUP_INET_SOCK_RELEASE));
     }
 
-    if (modules::sdklevel::IsAtLeastV()) {
+    if (isAtLeastV) {
         // V requires 4.19+, so technically this 2nd 'if' is not required, but it
         // doesn't hurt us to try to support AOSP forks that try to support older kernels.
-        if (bpf::isAtLeastKernelVersion(4, 19, 0)) {
+        if (isAtLeastKernelVersion(4, 19, 0)) {
             RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_CONNECT4_PROG_PATH,
                                         cg_fd, BPF_CGROUP_INET4_CONNECT));
             RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_CONNECT6_PROG_PATH,
@@ -138,7 +149,7 @@
                                         cg_fd, BPF_CGROUP_UDP6_SENDMSG));
         }
 
-        if (bpf::isAtLeastKernelVersion(5, 4, 0)) {
+        if (isAtLeastKernelVersion(5, 4, 0)) {
             RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_GETSOCKOPT_PROG_PATH,
                                         cg_fd, BPF_CGROUP_GETSOCKOPT));
             RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_SETSOCKOPT_PROG_PATH,
@@ -146,7 +157,7 @@
         }
     }
 
-    if (bpf::isAtLeastKernelVersion(4, 19, 0)) {
+    if (isAtLeastKernelVersion(4, 19, 0)) {
         RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_BIND4_PROG_PATH,
                 cg_fd, BPF_CGROUP_INET4_BIND));
         RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_BIND6_PROG_PATH,
@@ -154,32 +165,32 @@
 
         // This should trivially pass, since we just attached up above,
         // but BPF_PROG_QUERY is only implemented on 4.19+ kernels.
-        if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_EGRESS) <= 0) abort();
-        if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_INGRESS) <= 0) abort();
-        if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_SOCK_CREATE) <= 0) abort();
-        if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET4_BIND) <= 0) abort();
-        if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET6_BIND) <= 0) abort();
+        if (queryProgram(cg_fd, BPF_CGROUP_INET_EGRESS) <= 0) abort();
+        if (queryProgram(cg_fd, BPF_CGROUP_INET_INGRESS) <= 0) abort();
+        if (queryProgram(cg_fd, BPF_CGROUP_INET_SOCK_CREATE) <= 0) abort();
+        if (queryProgram(cg_fd, BPF_CGROUP_INET4_BIND) <= 0) abort();
+        if (queryProgram(cg_fd, BPF_CGROUP_INET6_BIND) <= 0) abort();
     }
 
-    if (bpf::isAtLeastKernelVersion(5, 10, 0)) {
-        if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_SOCK_RELEASE) <= 0) abort();
+    if (isAtLeastKernelVersion(5, 10, 0)) {
+        if (queryProgram(cg_fd, BPF_CGROUP_INET_SOCK_RELEASE) <= 0) abort();
     }
 
-    if (modules::sdklevel::IsAtLeastV()) {
+    if (isAtLeastV) {
         // V requires 4.19+, so technically this 2nd 'if' is not required, but it
         // doesn't hurt us to try to support AOSP forks that try to support older kernels.
-        if (bpf::isAtLeastKernelVersion(4, 19, 0)) {
-            if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET4_CONNECT) <= 0) abort();
-            if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET6_CONNECT) <= 0) abort();
-            if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP4_RECVMSG) <= 0) abort();
-            if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP6_RECVMSG) <= 0) abort();
-            if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP4_SENDMSG) <= 0) abort();
-            if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP6_SENDMSG) <= 0) abort();
+        if (isAtLeastKernelVersion(4, 19, 0)) {
+            if (queryProgram(cg_fd, BPF_CGROUP_INET4_CONNECT) <= 0) abort();
+            if (queryProgram(cg_fd, BPF_CGROUP_INET6_CONNECT) <= 0) abort();
+            if (queryProgram(cg_fd, BPF_CGROUP_UDP4_RECVMSG) <= 0) abort();
+            if (queryProgram(cg_fd, BPF_CGROUP_UDP6_RECVMSG) <= 0) abort();
+            if (queryProgram(cg_fd, BPF_CGROUP_UDP4_SENDMSG) <= 0) abort();
+            if (queryProgram(cg_fd, BPF_CGROUP_UDP6_SENDMSG) <= 0) abort();
         }
 
-        if (bpf::isAtLeastKernelVersion(5, 4, 0)) {
-            if (bpf::queryProgram(cg_fd, BPF_CGROUP_GETSOCKOPT) <= 0) abort();
-            if (bpf::queryProgram(cg_fd, BPF_CGROUP_SETSOCKOPT) <= 0) abort();
+        if (isAtLeastKernelVersion(5, 4, 0)) {
+            if (queryProgram(cg_fd, BPF_CGROUP_GETSOCKOPT) <= 0) abort();
+            if (queryProgram(cg_fd, BPF_CGROUP_SETSOCKOPT) <= 0) abort();
         }
     }
 
@@ -219,7 +230,7 @@
         // but there could be platform provided (xt_)bpf programs that oem/vendor
         // modified netd (which calls us during init) depends on...
         ALOGI("Waiting for platform BPF programs");
-        android::bpf::waitForProgsLoaded();
+        bpf::waitForProgsLoaded();
     }
 
     if (!mainlineNetBpfLoadDone()) {
@@ -251,12 +262,21 @@
     // ...unless someone changed 'exec_start bpfloader' to 'start bpfloader'
     // in the rc file.
     //
-    // TODO: should be: if (!modules::sdklevel::IsAtLeastW())
-    if (android_get_device_api_level() <= __ANDROID_API_V__) waitForBpf();
+    if (!isAtLeast25Q2) waitForBpf();
 
     RETURN_IF_NOT_OK(initPrograms(cg2_path));
     RETURN_IF_NOT_OK(initMaps());
 
+    if (isAtLeast25Q2) {
+        // Make sure netd can create & write maps.  sepolicy is V+, but enough to enforce on 25Q2+
+        int key = 1;
+        int value = 123;
+        unique_fd map(bpf::createMap(BPF_MAP_TYPE_ARRAY, sizeof(key), sizeof(value), 2, 0));
+        if (!map.ok()) return statusFromErrno(errno, fmt::format("map create failed"));
+        int rv = bpf::writeToMapEntry(map, &key, &value, BPF_ANY);
+        if (rv) return statusFromErrno(errno, fmt::format("map write failed (rv={})", rv));
+    }
+
     return netdutils::status::ok;
 }
 
@@ -283,7 +303,7 @@
 
 Status BpfHandler::initMaps() {
     // bpfLock() requires bpfGetFdMapId which is only available on 4.14+ kernels.
-    if (bpf::isAtLeastKernelVersion(4, 14, 0)) {
+    if (isAtLeastKernelVersion(4, 14, 0)) {
         mapLockTest();
     }
 
@@ -293,7 +313,6 @@
     RETURN_IF_NOT_OK(mUidPermissionMap.init(UID_PERMISSION_MAP_PATH));
     // initialized last so mCookieTagMap.isValid() implies everything else is valid too
     RETURN_IF_NOT_OK(mCookieTagMap.init(COOKIE_TAG_MAP_PATH));
-    ALOGI("%s successfully", __func__);
 
     return netdutils::status::ok;
 }
@@ -337,7 +356,7 @@
         return -errno;
     }
     if (socketFamily != AF_INET && socketFamily != AF_INET6) {
-        ALOGE("Unsupported family: %d", socketFamily);
+        ALOGV("Unsupported family: %d", socketFamily);
         return -EAFNOSUPPORT;
     }
 
@@ -348,7 +367,7 @@
         return -errno;
     }
     if (socketProto != IPPROTO_UDP && socketProto != IPPROTO_TCP) {
-        ALOGE("Unsupported protocol: %d", socketProto);
+        ALOGV("Unsupported protocol: %d", socketProto);
         return -EPROTONOSUPPORT;
     }
 
@@ -410,8 +429,8 @@
         ALOGE("Failed to tag the socket: %s", strerror(res.error().code()));
         return -res.error().code();
     }
-    ALOGD("Socket with cookie %" PRIu64 " tagged successfully with tag %" PRIu32 " uid %u "
-              "and real uid %u", sock_cookie, tag, chargeUid, realUid);
+    ALOGV("Socket with cookie %" PRIu64 " tagged successfully with tag %" PRIu32 " uid %u "
+          "and real uid %u", sock_cookie, tag, chargeUid, realUid);
     return 0;
 }
 
@@ -422,10 +441,11 @@
     if (!mCookieTagMap.isValid()) return -EPERM;
     base::Result<void> res = mCookieTagMap.deleteValue(sock_cookie);
     if (!res.ok()) {
-        ALOGE("Failed to untag socket: %s", strerror(res.error().code()));
-        return -res.error().code();
+        const int err = res.error().code();
+        if (err != ENOENT) ALOGE("Failed to untag socket: %s", strerror(err));
+        return -err;
     }
-    ALOGD("Socket with cookie %" PRIu64 " untagged successfully.", sock_cookie);
+    ALOGV("Socket with cookie %" PRIu64 " untagged successfully.", sock_cookie);
     return 0;
 }
 
diff --git a/bpf/progs/bpf_net_helpers.h b/bpf/progs/bpf_net_helpers.h
index a5664ba..4085ed4 100644
--- a/bpf/progs/bpf_net_helpers.h
+++ b/bpf/progs/bpf_net_helpers.h
@@ -84,6 +84,8 @@
 #define ETH_IP6_TCP_OFFSET(field) (ETH_HLEN + IP6_TCP_OFFSET(field))
 #define ETH_IP6_UDP_OFFSET(field) (ETH_HLEN + IP6_UDP_OFFSET(field))
 
+static uint64_t (*bpf_get_netns_cookie)(void* ctx) = (void*)BPF_FUNC_get_netns_cookie;
+
 // this returns 0 iff skb->sk is NULL
 static uint64_t (*bpf_get_socket_cookie)(struct __sk_buff* skb) = (void*)BPF_FUNC_get_socket_cookie;
 static uint64_t (*bpf_get_sk_cookie)(struct bpf_sock* sk) = (void*)BPF_FUNC_get_socket_cookie;
diff --git a/bpf/progs/netd.c b/bpf/progs/netd.c
index ed0eed5..08635b3 100644
--- a/bpf/progs/netd.c
+++ b/bpf/progs/netd.c
@@ -25,10 +25,6 @@
 static const int PASS = 1;
 static const int DROP_UNLESS_DNS = 2;  // internal to our program
 
-// Used for 'bool enable_tracing'
-static const bool TRACE_ON = true;
-static const bool TRACE_OFF = false;
-
 // offsetof(struct iphdr, ihl) -- but that's a bitfield
 #define IPPROTO_IHL_OFF 0
 
@@ -46,14 +42,14 @@
     DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries,         \
                        AID_ROOT, AID_NET_BW_ACCT, 0060, "fs_bpf_net_shared", "",   \
                        PRIVATE, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER,              \
-                       LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+                       LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG, 0)
 
 // For maps netd only needs read only access to
 #define DEFINE_BPF_MAP_RO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries)  \
     DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries,          \
                        AID_ROOT, AID_NET_BW_ACCT, 0460, "fs_bpf_netd_readonly", "", \
                        PRIVATE, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER,               \
-                       LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+                       LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG, 0)
 
 // For maps netd needs to be able to read and write
 #define DEFINE_BPF_MAP_RW_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
@@ -92,7 +88,7 @@
 DEFINE_BPF_MAP_EXT(packet_trace_enabled_map, ARRAY, uint32_t, bool, 1,
                    AID_ROOT, AID_SYSTEM, 0060, "fs_bpf_net_shared", "", PRIVATE,
                    BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG,
-                   LOAD_ON_USER, LOAD_ON_USERDEBUG)
+                   LOAD_ON_USER, LOAD_ON_USERDEBUG, 0)
 
 // A ring buffer on which packet information is pushed.
 DEFINE_BPF_RINGBUF_EXT(packet_trace_ringbuf, PacketTrace, PACKET_TRACE_BUF_SIZE,
@@ -103,6 +99,17 @@
 DEFINE_BPF_MAP_RO_NETD(data_saver_enabled_map, ARRAY, uint32_t, bool,
                        DATA_SAVER_ENABLED_MAP_SIZE)
 
+DEFINE_BPF_MAP_EXT(local_net_access_map, LPM_TRIE, LocalNetAccessKey, bool, 1000,
+                   AID_ROOT, AID_NET_BW_ACCT, 0060, "fs_bpf_net_shared", "", PRIVATE,
+                   BPFLOADER_MAINLINE_25Q2_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG, LOAD_ON_USER,
+                   LOAD_ON_USERDEBUG, 0)
+
+// not preallocated
+DEFINE_BPF_MAP_EXT(local_net_blocked_uid_map, HASH, uint32_t, bool, -1000,
+                   AID_ROOT, AID_NET_BW_ACCT, 0060, "fs_bpf_net_shared", "", PRIVATE,
+                   BPFLOADER_MAINLINE_25Q2_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG, LOAD_ON_USER,
+                   LOAD_ON_USERDEBUG, 0)
+
 // iptables xt_bpf programs need to be usable by both netd and netutils_wrappers
 // selinux contexts, because even non-xt_bpf iptables mutations are implemented as
 // a full table dump, followed by an update in userspace, and then a reload into the kernel,
@@ -110,31 +117,34 @@
 // program (see XT_BPF_MODE_PATH_PINNED) and then the iptables binary (or rather
 // the kernel acting on behalf of it) must be able to retrieve the pinned program
 // for the reload to succeed
-#define DEFINE_XTBPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
-    DEFINE_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog)
+#define DEFINE_XTBPF_PROG(SECTION_NAME, the_prog) \
+    DEFINE_BPF_PROG(SECTION_NAME, AID_ROOT, AID_NET_ADMIN, the_prog)
 
 // programs that need to be usable by netd, but not by netutils_wrappers
 // (this is because these are currently attached by the mainline provided libnetd_updatable .so
 // which is loaded into netd and thus runs as netd uid/gid/selinux context)
-#define DEFINE_NETD_BPF_PROG_KVER_RANGE(SECTION_NAME, prog_uid, prog_gid, the_prog, minKV, maxKV) \
-    DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog,                               \
-                        minKV, maxKV, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, MANDATORY,            \
+#define DEFINE_NETD_BPF_PROG_RANGES(SECTION_NAME, the_prog, minKV, maxKV, min_loader, max_loader) \
+    DEFINE_BPF_PROG_EXT(SECTION_NAME, AID_ROOT, AID_ROOT, the_prog,                               \
+                        minKV, maxKV, min_loader, max_loader, MANDATORY,                          \
                         "fs_bpf_netd_readonly", "", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
 
-#define DEFINE_NETD_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv) \
-    DEFINE_NETD_BPF_PROG_KVER_RANGE(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, KVER_INF)
+#define DEFINE_NETD_BPF_PROG_KVER_RANGE(SECTION_NAME, the_prog, minKV, maxKV) \
+    DEFINE_NETD_BPF_PROG_RANGES(SECTION_NAME, the_prog, minKV, maxKV, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER)
 
-#define DEFINE_NETD_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
-    DEFINE_NETD_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, KVER_NONE)
+#define DEFINE_NETD_BPF_PROG_KVER(SECTION_NAME, the_prog, min_kv) \
+    DEFINE_NETD_BPF_PROG_KVER_RANGE(SECTION_NAME, the_prog, min_kv, KVER_INF)
 
-#define DEFINE_NETD_V_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, minKV)            \
-    DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, minKV,                        \
+#define DEFINE_NETD_BPF_PROG(SECTION_NAME, the_prog) \
+    DEFINE_NETD_BPF_PROG_KVER(SECTION_NAME, the_prog, KVER_NONE)
+
+#define DEFINE_NETD_V_BPF_PROG_KVER(SECTION_NAME, the_prog, minKV)                                \
+    DEFINE_BPF_PROG_EXT(SECTION_NAME, AID_ROOT, AID_ROOT, the_prog, minKV,                        \
                         KVER_INF, BPFLOADER_MAINLINE_V_VERSION, BPFLOADER_MAX_VER, MANDATORY,     \
                         "fs_bpf_netd_readonly", "", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
 
 // programs that only need to be usable by the system server
-#define DEFINE_SYS_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
-    DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, KVER_NONE, KVER_INF,  \
+#define DEFINE_SYS_BPF_PROG(SECTION_NAME, the_prog) \
+    DEFINE_BPF_PROG_EXT(SECTION_NAME, AID_ROOT, AID_NET_ADMIN, the_prog, KVER_NONE, KVER_INF,  \
                         BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, MANDATORY, \
                         "fs_bpf_net_shared", "", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
 
@@ -231,11 +241,70 @@
         : bpf_skb_load_bytes(skb, L3_off, to, len);
 }
 
+// False iff arguments are found with longest prefix match lookup and disallowed.
+static inline __always_inline bool is_local_net_access_allowed(const uint32_t if_index,
+        const struct in6_addr* remote_ip6, const uint16_t protocol, const __be16 remote_port) {
+    LocalNetAccessKey query_key = {
+        .lpm_bitlen = 8 * (sizeof(if_index) + sizeof(*remote_ip6) + sizeof(protocol)
+            + sizeof(remote_port)),
+        .if_index = if_index,
+        .remote_ip6 = *remote_ip6,
+        .protocol = protocol,
+        .remote_port = remote_port
+    };
+    bool* v = bpf_local_net_access_map_lookup_elem(&query_key);
+    return v ? *v : true;
+}
+
+static __always_inline inline bool should_block_local_network_packets(struct __sk_buff *skb,
+                                   const uint32_t uid, const struct egress_bool egress,
+                                   const struct kver_uint kver) {
+    if (is_system_uid(uid)) return false;
+
+    bool* block_local_net = bpf_local_net_blocked_uid_map_lookup_elem(&uid);
+    if (!block_local_net) return false; // uid not found in map
+    if (!*block_local_net) return false; // lookup returned 'bool false'
+
+    struct in6_addr remote_ip6;
+    uint8_t ip_proto;
+    uint8_t L4_off;
+    if (skb->protocol == htons(ETH_P_IP)) {
+        int remote_ip_ofs = egress.egress ? IP4_OFFSET(daddr) : IP4_OFFSET(saddr);
+        remote_ip6.s6_addr32[0] = 0;
+        remote_ip6.s6_addr32[1] = 0;
+        remote_ip6.s6_addr32[2] = htonl(0xFFFF);
+        (void)bpf_skb_load_bytes_net(skb, remote_ip_ofs, &remote_ip6.s6_addr32[3], 4, kver);
+        (void)bpf_skb_load_bytes_net(skb, IP4_OFFSET(protocol), &ip_proto, sizeof(ip_proto), kver);
+        uint8_t ihl;
+        (void)bpf_skb_load_bytes_net(skb, IPPROTO_IHL_OFF, &ihl, sizeof(ihl), kver);
+        L4_off = (ihl & 0x0F) * 4;  // IHL calculation.
+    } else if (skb->protocol == htons(ETH_P_IPV6)) {
+        int remote_ip_ofs = egress.egress ? IP6_OFFSET(daddr) : IP6_OFFSET(saddr);
+        (void)bpf_skb_load_bytes_net(skb, remote_ip_ofs, &remote_ip6, sizeof(remote_ip6), kver);
+        (void)bpf_skb_load_bytes_net(skb, IP6_OFFSET(nexthdr), &ip_proto, sizeof(ip_proto), kver);
+        L4_off = sizeof(struct ipv6hdr);
+    } else {
+        return false;
+    }
+
+    __be16 remote_port = 0;
+    switch (ip_proto) {
+      case IPPROTO_TCP:
+      case IPPROTO_DCCP:
+      case IPPROTO_UDP:
+      case IPPROTO_UDPLITE:
+      case IPPROTO_SCTP:
+        (void)bpf_skb_load_bytes_net(skb, L4_off + (egress.egress ? 2 : 0), &remote_port, sizeof(remote_port), kver);
+        break;
+    }
+
+    return !is_local_net_access_allowed(skb->ifindex, &remote_ip6, ip_proto, remote_port);
+}
+
 static __always_inline inline void do_packet_tracing(
         const struct __sk_buff* const skb, const struct egress_bool egress, const uint32_t uid,
-        const uint32_t tag, const bool enable_tracing, const struct kver_uint kver) {
-    if (!enable_tracing) return;
-    if (!KVER_IS_AT_LEAST(kver, 5, 8, 0)) return;
+        const uint32_t tag, const struct kver_uint kver) {
+    if (!KVER_IS_AT_LEAST(kver, 5, 10, 0)) return;
 
     uint32_t mapKey = 0;
     bool* traceConfig = bpf_packet_trace_enabled_map_lookup_elem(&mapKey);
@@ -393,7 +462,8 @@
 
 static __always_inline inline int bpf_owner_match(struct __sk_buff* skb, uint32_t uid,
                                                   const struct egress_bool egress,
-                                                  const struct kver_uint kver) {
+                                                  const struct kver_uint kver,
+                                                  const struct sdk_level_uint lvl) {
     if (is_system_uid(uid)) return PASS;
 
     if (skip_owner_match(skb, egress, kver)) return PASS;
@@ -423,6 +493,11 @@
             return DROP_UNLESS_DNS;
         }
     }
+
+    if (SDK_LEVEL_IS_AT_LEAST(lvl, 25Q2) && skb->ifindex == 1) {
+        // TODO: sdksandbox localhost restrictions
+    }
+
     return PASS;
 }
 
@@ -440,8 +515,8 @@
 
 static __always_inline inline int bpf_traffic_account(struct __sk_buff* skb,
                                                       const struct egress_bool egress,
-                                                      const bool enable_tracing,
-                                                      const struct kver_uint kver) {
+                                                      const struct kver_uint kver,
+                                                      const struct sdk_level_uint lvl) {
     // sock_uid will be 'overflowuid' if !sk_fullsock(sk_to_full_sk(skb->sk))
     uint32_t sock_uid = bpf_get_socket_uid(skb);
 
@@ -470,7 +545,7 @@
     // CLAT daemon receives via an untagged AF_PACKET socket.
     if (egress.egress && uid == AID_CLAT) return PASS;
 
-    int match = bpf_owner_match(skb, sock_uid, egress, kver);
+    int match = bpf_owner_match(skb, sock_uid, egress, kver, lvl);
 
 // Workaround for secureVPN with VpnIsolation enabled, refer to b/159994981 for details.
 // Keep TAG_SYSTEM_DNS in sync with DnsResolver/include/netd_resolv/resolv.h
@@ -483,6 +558,10 @@
         if (match == DROP_UNLESS_DNS) match = DROP;
     }
 
+    if (SDK_LEVEL_IS_AT_LEAST(lvl, 25Q2) && (match != DROP)) {
+        if (should_block_local_network_packets(skb, uid, egress, kver)) match = DROP;
+    }
+
     // If an outbound packet is going to be dropped, we do not count that traffic.
     if (egress.egress && (match == DROP)) return DROP;
 
@@ -496,7 +575,7 @@
 
     if (!selectedMap) return PASS;  // cannot happen, needed to keep bpf verifier happy
 
-    do_packet_tracing(skb, egress, uid, tag, enable_tracing, kver);
+    do_packet_tracing(skb, egress, uid, tag, kver);
     update_stats_with_config(*selectedMap, skb, &key, egress, kver);
     update_app_uid_stats_map(skb, &uid, egress, kver);
 
@@ -509,52 +588,104 @@
     return match;
 }
 
-// Tracing on Android U+ 5.8+
-DEFINE_BPF_PROG_EXT("cgroupskb/ingress/stats$trace", AID_ROOT, AID_SYSTEM,
-                    bpf_cgroup_ingress_trace, KVER_5_8, KVER_INF,
-                    BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, MANDATORY,
-                    "fs_bpf_netd_readonly", "",
-                    LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+// -----
+
+// Supported kernel + platform/os version combinations:
+//
+//      | 4.9 | 4.14 | 4.19 | 5.4 | 5.10 | 5.15 | 6.1 | 6.6 | 6.12 |
+// 25Q2 |     |      |      |  x  |  x   |  x   |  x  |  x  |  x   |
+//    V |     |      |  x   |  x  |  x   |  x   |  x  |  x  |      | (netbpfload)
+//    U |     |  x   |  x   |  x  |  x   |  x   |  x  |     |      |
+//    T |  x  |  x   |  x   |  x  |  x   |  x   |     |     |      | (magic netbpfload)
+//    S |  x  |  x   |  x   |  x  |  x   |      |     |     |      | (platform loads offload)
+//    R |  x  |  x   |  x   |  x  |      |      |     |     |      | (no mainline ebpf)
+//
+// Not relevant for eBPF, but R can also run on 4.4
+
+// ----- cgroupskb/ingress/stats -----
+
+// Android 25Q2+ 5.10+ (localnet protection + tracing)
+DEFINE_NETD_BPF_PROG_RANGES("cgroupskb/ingress/stats$5_10_25q2",
+                            bpf_cgroup_ingress_5_10_25q2, KVER_5_10, KVER_INF,
+                            BPFLOADER_MAINLINE_25Q2_VERSION, BPFLOADER_MAX_VER)
 (struct __sk_buff* skb) {
-    return bpf_traffic_account(skb, INGRESS, TRACE_ON, KVER_5_8);
+    return bpf_traffic_account(skb, INGRESS, KVER_5_10, SDK_LEVEL_25Q2);
 }
 
-DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/ingress/stats$4_19", AID_ROOT, AID_SYSTEM,
+// Android 25Q2+ 5.4 (localnet protection)
+DEFINE_NETD_BPF_PROG_RANGES("cgroupskb/ingress/stats$5_4_25q2",
+                            bpf_cgroup_ingress_5_4_25q2, KVER_5_4, KVER_5_10,
+                            BPFLOADER_MAINLINE_25Q2_VERSION, BPFLOADER_MAX_VER)
+(struct __sk_buff* skb) {
+    return bpf_traffic_account(skb, INGRESS, KVER_5_4, SDK_LEVEL_25Q2);
+}
+
+// Android U/V 5.10+ (tracing)
+DEFINE_NETD_BPF_PROG_RANGES("cgroupskb/ingress/stats$5_10_u",
+                            bpf_cgroup_ingress_5_10_u, KVER_5_10, KVER_INF,
+                            BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAINLINE_25Q2_VERSION)
+(struct __sk_buff* skb) {
+    return bpf_traffic_account(skb, INGRESS, KVER_5_10, SDK_LEVEL_U);
+}
+
+// Android T/U/V 4.19 & T/U/V/25Q2 5.4 & T 5.10/5.15
+DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/ingress/stats$4_19",
                                 bpf_cgroup_ingress_4_19, KVER_4_19, KVER_INF)
 (struct __sk_buff* skb) {
-    return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER_4_19);
+    return bpf_traffic_account(skb, INGRESS, KVER_4_19, SDK_LEVEL_T);
 }
 
-DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/ingress/stats$4_14", AID_ROOT, AID_SYSTEM,
-                                bpf_cgroup_ingress_4_14, KVER_NONE, KVER_4_19)
+// Android T 4.9 & T/U 4.14
+DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/ingress/stats$4_9",
+                                bpf_cgroup_ingress_4_9, KVER_NONE, KVER_4_19)
 (struct __sk_buff* skb) {
-    return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER_NONE);
+    return bpf_traffic_account(skb, INGRESS, KVER_NONE, SDK_LEVEL_T);
 }
 
-// Tracing on Android U+ 5.8+
-DEFINE_BPF_PROG_EXT("cgroupskb/egress/stats$trace", AID_ROOT, AID_SYSTEM,
-                    bpf_cgroup_egress_trace, KVER_5_8, KVER_INF,
-                    BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, MANDATORY,
-                    "fs_bpf_netd_readonly", "",
-                    LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+// ----- cgroupskb/egress/stats -----
+
+// Android 25Q2+ 5.10+ (localnet protection + tracing)
+DEFINE_NETD_BPF_PROG_RANGES("cgroupskb/egress/stats$5_10_25q2",
+                            bpf_cgroup_egress_5_10_25q2, KVER_5_10, KVER_INF,
+                            BPFLOADER_MAINLINE_25Q2_VERSION, BPFLOADER_MAX_VER)
 (struct __sk_buff* skb) {
-    return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER_5_8);
+    return bpf_traffic_account(skb, EGRESS, KVER_5_10, SDK_LEVEL_25Q2);
 }
 
-DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/egress/stats$4_19", AID_ROOT, AID_SYSTEM,
+// Android 25Q2+ 5.4 (localnet protection)
+DEFINE_NETD_BPF_PROG_RANGES("cgroupskb/egress/stats$5_4_25q2",
+                            bpf_cgroup_egress_5_4_25q2, KVER_5_4, KVER_5_10,
+                            BPFLOADER_MAINLINE_25Q2_VERSION, BPFLOADER_MAX_VER)
+(struct __sk_buff* skb) {
+    return bpf_traffic_account(skb, EGRESS, KVER_5_4, SDK_LEVEL_25Q2);
+}
+
+// Android U/V 5.10+ (tracing)
+DEFINE_NETD_BPF_PROG_RANGES("cgroupskb/egress/stats$5_10_u",
+                            bpf_cgroup_egress_5_10_u, KVER_5_10, KVER_INF,
+                            BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAINLINE_25Q2_VERSION)
+(struct __sk_buff* skb) {
+    return bpf_traffic_account(skb, EGRESS, KVER_5_10, SDK_LEVEL_U);
+}
+
+// Android T/U/V 4.19 & T/U/V/25Q2 5.4 & T 5.10/5.15
+DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/egress/stats$4_19",
                                 bpf_cgroup_egress_4_19, KVER_4_19, KVER_INF)
 (struct __sk_buff* skb) {
-    return bpf_traffic_account(skb, EGRESS, TRACE_OFF, KVER_4_19);
+    return bpf_traffic_account(skb, EGRESS, KVER_4_19, SDK_LEVEL_T);
 }
 
-DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/egress/stats$4_14", AID_ROOT, AID_SYSTEM,
-                                bpf_cgroup_egress_4_14, KVER_NONE, KVER_4_19)
+// Android T 4.9 & T/U 4.14
+DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/egress/stats$4_9",
+                                bpf_cgroup_egress_4_9, KVER_NONE, KVER_4_19)
 (struct __sk_buff* skb) {
-    return bpf_traffic_account(skb, EGRESS, TRACE_OFF, KVER_NONE);
+    return bpf_traffic_account(skb, EGRESS, KVER_NONE, SDK_LEVEL_T);
 }
 
+// -----
+
 // WARNING: Android T's non-updatable netd depends on the name of this program.
-DEFINE_XTBPF_PROG("skfilter/egress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_egress_prog)
+DEFINE_XTBPF_PROG("skfilter/egress/xtbpf", xt_bpf_egress_prog)
 (struct __sk_buff* skb) {
     // Clat daemon does not generate new traffic, all its traffic is accounted for already
     // on the v4-* interfaces (except for the 20 (or 28) extra bytes of IPv6 vs IPv4 overhead,
@@ -573,7 +704,7 @@
 }
 
 // WARNING: Android T's non-updatable netd depends on the name of this program.
-DEFINE_XTBPF_PROG("skfilter/ingress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_ingress_prog)
+DEFINE_XTBPF_PROG("skfilter/ingress/xtbpf", xt_bpf_ingress_prog)
 (struct __sk_buff* skb) {
     // Clat daemon traffic is not accounted by virtue of iptables raw prerouting drop rule
     // (in clat_raw_PREROUTING chain), which triggers before this (in bw_raw_PREROUTING chain).
@@ -585,7 +716,7 @@
     return XTBPF_MATCH;
 }
 
-DEFINE_SYS_BPF_PROG("schedact/ingress/account", AID_ROOT, AID_NET_ADMIN,
+DEFINE_SYS_BPF_PROG("schedact/ingress/account",
                     tc_bpf_ingress_account_prog)
 (struct __sk_buff* skb) {
     if (is_received_skb(skb)) {
@@ -597,7 +728,7 @@
 }
 
 // WARNING: Android T's non-updatable netd depends on the name of this program.
-DEFINE_XTBPF_PROG("skfilter/allowlist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_allowlist_prog)
+DEFINE_XTBPF_PROG("skfilter/allowlist/xtbpf", xt_bpf_allowlist_prog)
 (struct __sk_buff* skb) {
     uint32_t sock_uid = bpf_get_socket_uid(skb);
     if (is_system_uid(sock_uid)) return XTBPF_MATCH;
@@ -616,7 +747,7 @@
 }
 
 // WARNING: Android T's non-updatable netd depends on the name of this program.
-DEFINE_XTBPF_PROG("skfilter/denylist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_denylist_prog)
+DEFINE_XTBPF_PROG("skfilter/denylist/xtbpf", xt_bpf_denylist_prog)
 (struct __sk_buff* skb) {
     uint32_t sock_uid = bpf_get_socket_uid(skb);
     UidOwnerValue* denylistMatch = bpf_uid_owner_map_lookup_elem(&sock_uid);
@@ -639,14 +770,12 @@
     return permissions ? *permissions : BPF_PERMISSION_INTERNET;
 }
 
-DEFINE_NETD_BPF_PROG_KVER("cgroupsock/inet_create", AID_ROOT, AID_ROOT, inet_socket_create,
-                          KVER_4_14)
+DEFINE_NETD_BPF_PROG_KVER("cgroupsock/inet_create", inet_socket_create, KVER_4_14)
 (__unused struct bpf_sock* sk) {
     return (get_app_permissions() & BPF_PERMISSION_INTERNET) ? BPF_ALLOW : BPF_DISALLOW;
 }
 
-DEFINE_NETD_BPF_PROG_KVER("cgroupsockrelease/inet_release", AID_ROOT, AID_ROOT,
-                          inet_socket_release, KVER_5_10)
+DEFINE_NETD_BPF_PROG_KVER("cgroupsockrelease/inet_release", inet_socket_release, KVER_5_10)
 (struct bpf_sock* sk) {
     uint64_t cookie = bpf_get_sk_cookie(sk);
     if (cookie) bpf_cookie_tag_map_delete_elem(&cookie);
@@ -699,47 +828,47 @@
     return BPF_ALLOW;
 }
 
-DEFINE_NETD_BPF_PROG_KVER("bind4/inet4_bind", AID_ROOT, AID_ROOT, inet4_bind, KVER_4_19)
+DEFINE_NETD_BPF_PROG_KVER("bind4/inet4_bind", inet4_bind, KVER_4_19)
 (struct bpf_sock_addr *ctx) {
     return block_port(ctx);
 }
 
-DEFINE_NETD_BPF_PROG_KVER("bind6/inet6_bind", AID_ROOT, AID_ROOT, inet6_bind, KVER_4_19)
+DEFINE_NETD_BPF_PROG_KVER("bind6/inet6_bind", inet6_bind, KVER_4_19)
 (struct bpf_sock_addr *ctx) {
     return block_port(ctx);
 }
 
-DEFINE_NETD_V_BPF_PROG_KVER("connect4/inet4_connect", AID_ROOT, AID_ROOT, inet4_connect, KVER_4_19)
+DEFINE_NETD_V_BPF_PROG_KVER("connect4/inet4_connect", inet4_connect, KVER_4_19)
 (struct bpf_sock_addr *ctx) {
     return check_localhost(ctx);
 }
 
-DEFINE_NETD_V_BPF_PROG_KVER("connect6/inet6_connect", AID_ROOT, AID_ROOT, inet6_connect, KVER_4_19)
+DEFINE_NETD_V_BPF_PROG_KVER("connect6/inet6_connect", inet6_connect, KVER_4_19)
 (struct bpf_sock_addr *ctx) {
     return check_localhost(ctx);
 }
 
-DEFINE_NETD_V_BPF_PROG_KVER("recvmsg4/udp4_recvmsg", AID_ROOT, AID_ROOT, udp4_recvmsg, KVER_4_19)
+DEFINE_NETD_V_BPF_PROG_KVER("recvmsg4/udp4_recvmsg", udp4_recvmsg, KVER_4_19)
 (struct bpf_sock_addr *ctx) {
     return check_localhost(ctx);
 }
 
-DEFINE_NETD_V_BPF_PROG_KVER("recvmsg6/udp6_recvmsg", AID_ROOT, AID_ROOT, udp6_recvmsg, KVER_4_19)
+DEFINE_NETD_V_BPF_PROG_KVER("recvmsg6/udp6_recvmsg", udp6_recvmsg, KVER_4_19)
 (struct bpf_sock_addr *ctx) {
     return check_localhost(ctx);
 }
 
-DEFINE_NETD_V_BPF_PROG_KVER("sendmsg4/udp4_sendmsg", AID_ROOT, AID_ROOT, udp4_sendmsg, KVER_4_19)
+DEFINE_NETD_V_BPF_PROG_KVER("sendmsg4/udp4_sendmsg", udp4_sendmsg, KVER_4_19)
 (struct bpf_sock_addr *ctx) {
     return check_localhost(ctx);
 }
 
-DEFINE_NETD_V_BPF_PROG_KVER("sendmsg6/udp6_sendmsg", AID_ROOT, AID_ROOT, udp6_sendmsg, KVER_4_19)
+DEFINE_NETD_V_BPF_PROG_KVER("sendmsg6/udp6_sendmsg", udp6_sendmsg, KVER_4_19)
 (struct bpf_sock_addr *ctx) {
     return check_localhost(ctx);
 }
 
-DEFINE_NETD_V_BPF_PROG_KVER("getsockopt/prog", AID_ROOT, AID_ROOT, getsockopt_prog, KVER_5_4)
+DEFINE_NETD_V_BPF_PROG_KVER("getsockopt/prog", getsockopt_prog, KVER_5_4)
 (struct bpf_sockopt *ctx) {
     // Tell kernel to return 'original' kernel reply (instead of the bpf modified buffer)
     // This is important if the answer is larger than PAGE_SIZE (max size this bpf hook can provide)
@@ -747,7 +876,7 @@
     return BPF_ALLOW;
 }
 
-DEFINE_NETD_V_BPF_PROG_KVER("setsockopt/prog", AID_ROOT, AID_ROOT, setsockopt_prog, KVER_5_4)
+DEFINE_NETD_V_BPF_PROG_KVER("setsockopt/prog", setsockopt_prog, KVER_5_4)
 (struct bpf_sockopt *ctx) {
     // Tell kernel to use/process original buffer provided by userspace.
     // This is important if it is larger than PAGE_SIZE (max size this bpf hook can handle).
diff --git a/bpf/progs/netd.h b/bpf/progs/netd.h
index be7c311..8400679 100644
--- a/bpf/progs/netd.h
+++ b/bpf/progs/netd.h
@@ -185,6 +185,8 @@
 #define PACKET_TRACE_RINGBUF_PATH BPF_NETD_PATH "map_netd_packet_trace_ringbuf"
 #define PACKET_TRACE_ENABLED_MAP_PATH BPF_NETD_PATH "map_netd_packet_trace_enabled_map"
 #define DATA_SAVER_ENABLED_MAP_PATH BPF_NETD_PATH "map_netd_data_saver_enabled_map"
+#define LOCAL_NET_ACCESS_MAP_PATH BPF_NETD_PATH "map_netd_local_net_access_map"
+#define LOCAL_NET_BLOCKED_UID_MAP_PATH BPF_NETD_PATH "map_netd_local_net_blocked_uid_map"
 
 #endif // __cplusplus
 
@@ -245,6 +247,18 @@
 } IngressDiscardValue;
 STRUCT_SIZE(IngressDiscardValue, 2 * 4);  // 8
 
+typedef struct {
+  // Longest prefix match length in bits (value from 0 to 192).
+  uint32_t lpm_bitlen;
+  uint32_t if_index;
+  // IPv4 uses IPv4-mapped IPv6 address format.
+  struct in6_addr remote_ip6;
+  // u16 instead of u8 to avoid padding due to alignment requirement.
+  uint16_t protocol;
+  __be16 remote_port;
+} LocalNetAccessKey;
+STRUCT_SIZE(LocalNetAccessKey, 4 + 4 + 16 + 2 + 2);  // 28
+
 // Entry in the configuration map that stores which UID rules are enabled.
 #define UID_RULES_CONFIGURATION_KEY 0
 // Entry in the configuration map that stores which stats map is currently in use.
diff --git a/bpf/progs/offload.c b/bpf/progs/offload.c
index 631908a..0f23844 100644
--- a/bpf/progs/offload.c
+++ b/bpf/progs/offload.c
@@ -609,27 +609,27 @@
 // Full featured (required) implementations for 5.8+ kernels (these are S+ by definition)
 
 DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_rawip$5_8", AID_ROOT, AID_NETWORK_STACK,
-                     sched_cls_tether_downstream4_rawip_5_8, KVER_5_8)
+                     sched_cls_tether_downstream4_rawip_5_8, KVER_5_10)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER_5_8);
+    return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER_5_10);
 }
 
 DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_rawip$5_8", AID_ROOT, AID_NETWORK_STACK,
-                     sched_cls_tether_upstream4_rawip_5_8, KVER_5_8)
+                     sched_cls_tether_upstream4_rawip_5_8, KVER_5_10)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER_5_8);
+    return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER_5_10);
 }
 
 DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_ether$5_8", AID_ROOT, AID_NETWORK_STACK,
-                     sched_cls_tether_downstream4_ether_5_8, KVER_5_8)
+                     sched_cls_tether_downstream4_ether_5_8, KVER_5_10)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER_5_8);
+    return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER_5_10);
 }
 
 DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_ether$5_8", AID_ROOT, AID_NETWORK_STACK,
-                     sched_cls_tether_upstream4_ether_5_8, KVER_5_8)
+                     sched_cls_tether_upstream4_ether_5_8, KVER_5_10)
 (struct __sk_buff* skb) {
-    return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER_5_8);
+    return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER_5_10);
 }
 
 // Full featured (optional) implementations for 4.14-S, 4.19-S & 5.4-S kernels
@@ -638,7 +638,7 @@
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$opt",
                                     AID_ROOT, AID_NETWORK_STACK,
                                     sched_cls_tether_downstream4_rawip_opt,
-                                    KVER_4_14, KVER_5_8)
+                                    KVER_4_14, KVER_5_10)
 (struct __sk_buff* skb) {
     return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER_4_14);
 }
@@ -646,7 +646,7 @@
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$opt",
                                     AID_ROOT, AID_NETWORK_STACK,
                                     sched_cls_tether_upstream4_rawip_opt,
-                                    KVER_4_14, KVER_5_8)
+                                    KVER_4_14, KVER_5_10)
 (struct __sk_buff* skb) {
     return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER_4_14);
 }
@@ -654,7 +654,7 @@
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$opt",
                                     AID_ROOT, AID_NETWORK_STACK,
                                     sched_cls_tether_downstream4_ether_opt,
-                                    KVER_4_14, KVER_5_8)
+                                    KVER_4_14, KVER_5_10)
 (struct __sk_buff* skb) {
     return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER_4_14);
 }
@@ -662,7 +662,7 @@
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$opt",
                                     AID_ROOT, AID_NETWORK_STACK,
                                     sched_cls_tether_upstream4_ether_opt,
-                                    KVER_4_14, KVER_5_8)
+                                    KVER_4_14, KVER_5_10)
 (struct __sk_buff* skb) {
     return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER_4_14);
 }
@@ -682,13 +682,13 @@
 // RAWIP: Required for 5.4-R kernels -- which always support bpf_skb_change_head().
 
 DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$5_4", AID_ROOT, AID_NETWORK_STACK,
-                           sched_cls_tether_downstream4_rawip_5_4, KVER_5_4, KVER_5_8)
+                           sched_cls_tether_downstream4_rawip_5_4, KVER_5_4, KVER_5_10)
 (struct __sk_buff* skb) {
     return do_forward4(skb, RAWIP, DOWNSTREAM, NO_UPDATETIME, KVER_5_4);
 }
 
 DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$5_4", AID_ROOT, AID_NETWORK_STACK,
-                           sched_cls_tether_upstream4_rawip_5_4, KVER_5_4, KVER_5_8)
+                           sched_cls_tether_upstream4_rawip_5_4, KVER_5_4, KVER_5_10)
 (struct __sk_buff* skb) {
     return do_forward4(skb, RAWIP, UPSTREAM, NO_UPDATETIME, KVER_5_4);
 }
@@ -715,13 +715,13 @@
 // ETHER: Required for 4.14-Q/R, 4.19-Q/R & 5.4-R kernels.
 
 DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$4_14", AID_ROOT, AID_NETWORK_STACK,
-                           sched_cls_tether_downstream4_ether_4_14, KVER_4_14, KVER_5_8)
+                           sched_cls_tether_downstream4_ether_4_14, KVER_4_14, KVER_5_10)
 (struct __sk_buff* skb) {
     return do_forward4(skb, ETHER, DOWNSTREAM, NO_UPDATETIME, KVER_4_14);
 }
 
 DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$4_14", AID_ROOT, AID_NETWORK_STACK,
-                           sched_cls_tether_upstream4_ether_4_14, KVER_4_14, KVER_5_8)
+                           sched_cls_tether_upstream4_ether_4_14, KVER_4_14, KVER_5_10)
 (struct __sk_buff* skb) {
     return do_forward4(skb, ETHER, UPSTREAM, NO_UPDATETIME, KVER_4_14);
 }
@@ -805,7 +805,7 @@
 }
 
 #define DEFINE_XDP_PROG(str, func) \
-    DEFINE_BPF_PROG_KVER(str, AID_ROOT, AID_NETWORK_STACK, func, KVER_5_9)(struct xdp_md *ctx)
+    DEFINE_BPF_PROG_KVER(str, AID_ROOT, AID_NETWORK_STACK, func, KVER_5_10)(struct xdp_md *ctx)
 
 DEFINE_XDP_PROG("xdp/tether_downstream_ether",
                  xdp_tether_downstream_ether) {
diff --git a/bpf/syscall_wrappers/include/BpfSyscallWrappers.h b/bpf/syscall_wrappers/include/BpfSyscallWrappers.h
index a31445a..1d72b77 100644
--- a/bpf/syscall_wrappers/include/BpfSyscallWrappers.h
+++ b/bpf/syscall_wrappers/include/BpfSyscallWrappers.h
@@ -269,7 +269,7 @@
     return info.FIELD; \
 }
 
-// All 7 of these fields are already present in Linux v4.14 (even ACK 4.14-P)
+// All 9 of these fields are already present in Linux v4.14 (even ACK 4.14-P)
 // while BPF_OBJ_GET_INFO_BY_FD is not implemented at all in v4.9 (even ACK 4.9-Q)
 DEFINE_BPF_GET_FD(map, MapType, type)            // int bpfGetFdMapType(const borrowed_fd& map_fd)
 DEFINE_BPF_GET_FD(map, MapId, id)                // int bpfGetFdMapId(const borrowed_fd& map_fd)
@@ -278,6 +278,8 @@
 DEFINE_BPF_GET_FD(map, MaxEntries, max_entries)  // int bpfGetFdMaxEntries(const borrowed_fd& map_fd)
 DEFINE_BPF_GET_FD(map, MapFlags, map_flags)      // int bpfGetFdMapFlags(const borrowed_fd& map_fd)
 DEFINE_BPF_GET_FD(prog, ProgId, id)              // int bpfGetFdProgId(const borrowed_fd& prog_fd)
+DEFINE_BPF_GET_FD(prog, JitProgLen, jited_prog_len)   // int bpfGetFdJitProgLen(...)
+DEFINE_BPF_GET_FD(prog, XlatProgLen, xlated_prog_len) // int bpfGetFdXlatProgLen(...)
 
 #undef DEFINE_BPF_GET_FD
 
diff --git a/bpf/tests/mts/bpf_existence_test.cpp b/bpf/tests/mts/bpf_existence_test.cpp
index 0b5b7be..75fb8e9 100644
--- a/bpf/tests/mts/bpf_existence_test.cpp
+++ b/bpf/tests/mts/bpf_existence_test.cpp
@@ -20,7 +20,8 @@
 #include <set>
 #include <string>
 
-#include <android-modules-utils/sdk_level.h>
+#include <android-base/properties.h>
+#include <android/api-level.h>
 #include <bpf/BpfUtils.h>
 
 #include <gtest/gtest.h>
@@ -30,11 +31,12 @@
 using std::string;
 
 using android::bpf::isAtLeastKernelVersion;
-using android::modules::sdklevel::IsAtLeastR;
-using android::modules::sdklevel::IsAtLeastS;
-using android::modules::sdklevel::IsAtLeastT;
-using android::modules::sdklevel::IsAtLeastU;
-using android::modules::sdklevel::IsAtLeastV;
+using android::bpf::isAtLeastR;
+using android::bpf::isAtLeastS;
+using android::bpf::isAtLeastT;
+using android::bpf::isAtLeastU;
+using android::bpf::isAtLeastV;
+using android::bpf::isAtLeast25Q2;
 
 #define PLATFORM "/sys/fs/bpf/"
 #define TETHERING "/sys/fs/bpf/tethering/"
@@ -159,6 +161,12 @@
     NETD "prog_netd_setsockopt_prog",
 };
 
+// Provided by *current* mainline module for 25Q2+ devices
+static const set<string> MAINLINE_FOR_25Q2_PLUS = {
+    NETD "map_netd_local_net_access_map",
+    NETD "map_netd_local_net_blocked_uid_map",
+};
+
 static void addAll(set<string>& a, const set<string>& b) {
     a.insert(b.begin(), b.end());
 }
@@ -181,33 +189,36 @@
     // and for the presence of mainline stuff.
 
     // Note: Q is no longer supported by mainline
-    ASSERT_TRUE(IsAtLeastR());
+    ASSERT_TRUE(isAtLeastR);
 
     // R can potentially run on pre-4.9 kernel non-eBPF capable devices.
-    DO_EXPECT(IsAtLeastR() && !IsAtLeastS() && isAtLeastKernelVersion(4, 9, 0), PLATFORM_ONLY_IN_R);
+    DO_EXPECT(isAtLeastR && !isAtLeastS && isAtLeastKernelVersion(4, 9, 0), PLATFORM_ONLY_IN_R);
 
     // S requires Linux Kernel 4.9+ and thus requires eBPF support.
-    if (IsAtLeastS()) ASSERT_TRUE(isAtLeastKernelVersion(4, 9, 0));
-    DO_EXPECT(IsAtLeastS(), MAINLINE_FOR_S_PLUS);
+    if (isAtLeastS) ASSERT_TRUE(isAtLeastKernelVersion(4, 9, 0));
+    DO_EXPECT(isAtLeastS, MAINLINE_FOR_S_PLUS);
 
     // Nothing added or removed in SCv2.
 
     // T still only requires Linux Kernel 4.9+.
-    DO_EXPECT(IsAtLeastT(), MAINLINE_FOR_T_PLUS);
-    DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(4, 14, 0), MAINLINE_FOR_T_4_14_PLUS);
-    DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(4, 19, 0), MAINLINE_FOR_T_4_19_PLUS);
-    DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(5, 10, 0), MAINLINE_FOR_T_5_10_PLUS);
-    DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(5, 15, 0), MAINLINE_FOR_T_5_15_PLUS);
+    DO_EXPECT(isAtLeastT, MAINLINE_FOR_T_PLUS);
+    DO_EXPECT(isAtLeastT && isAtLeastKernelVersion(4, 14, 0), MAINLINE_FOR_T_4_14_PLUS);
+    DO_EXPECT(isAtLeastT && isAtLeastKernelVersion(4, 19, 0), MAINLINE_FOR_T_4_19_PLUS);
+    DO_EXPECT(isAtLeastT && isAtLeastKernelVersion(5, 10, 0), MAINLINE_FOR_T_5_10_PLUS);
+    DO_EXPECT(isAtLeastT && isAtLeastKernelVersion(5, 15, 0), MAINLINE_FOR_T_5_15_PLUS);
 
     // U requires Linux Kernel 4.14+, but nothing (as yet) added or removed in U.
-    if (IsAtLeastU()) ASSERT_TRUE(isAtLeastKernelVersion(4, 14, 0));
-    DO_EXPECT(IsAtLeastU(), MAINLINE_FOR_U_PLUS);
-    DO_EXPECT(IsAtLeastU() && isAtLeastKernelVersion(5, 10, 0), MAINLINE_FOR_U_5_10_PLUS);
+    if (isAtLeastU) ASSERT_TRUE(isAtLeastKernelVersion(4, 14, 0));
+    DO_EXPECT(isAtLeastU, MAINLINE_FOR_U_PLUS);
+    DO_EXPECT(isAtLeastU && isAtLeastKernelVersion(5, 10, 0), MAINLINE_FOR_U_5_10_PLUS);
 
     // V requires Linux Kernel 4.19+, but nothing (as yet) added or removed in V.
-    if (IsAtLeastV()) ASSERT_TRUE(isAtLeastKernelVersion(4, 19, 0));
-    DO_EXPECT(IsAtLeastV(), MAINLINE_FOR_V_PLUS);
-    DO_EXPECT(IsAtLeastV() && isAtLeastKernelVersion(5, 4, 0), MAINLINE_FOR_V_5_4_PLUS);
+    if (isAtLeastV) ASSERT_TRUE(isAtLeastKernelVersion(4, 19, 0));
+    DO_EXPECT(isAtLeastV, MAINLINE_FOR_V_PLUS);
+    DO_EXPECT(isAtLeastV && isAtLeastKernelVersion(5, 4, 0), MAINLINE_FOR_V_5_4_PLUS);
+
+    if (isAtLeast25Q2) ASSERT_TRUE(isAtLeastKernelVersion(5, 4, 0));
+    DO_EXPECT(isAtLeast25Q2, MAINLINE_FOR_25Q2_PLUS);
 
     for (const auto& file : mustExist) {
         EXPECT_EQ(0, access(file.c_str(), R_OK)) << file << " does not exist";
diff --git a/clatd/main.c b/clatd/main.c
index f888041..7aa1671 100644
--- a/clatd/main.c
+++ b/clatd/main.c
@@ -37,7 +37,7 @@
 /* function: stop_loop
  * signal handler: stop the event loop
  */
-static void stop_loop() { running = 0; };
+static void stop_loop(__attribute__((unused)) int unused) { running = 0; };
 
 /* function: print_help
  * in case the user is running this on the command line
diff --git a/common/FlaggedApi.bp b/common/FlaggedApi.bp
index 39ff2d4..f89ff9d 100644
--- a/common/FlaggedApi.bp
+++ b/common/FlaggedApi.bp
@@ -17,6 +17,7 @@
 aconfig_declarations {
     name: "com.android.net.flags-aconfig",
     package: "com.android.net.flags",
+    exportable: true,
     container: "com.android.tethering",
     srcs: ["flags.aconfig"],
     visibility: ["//packages/modules/Connectivity:__subpackages__"],
@@ -32,6 +33,17 @@
     ],
 }
 
+java_aconfig_library {
+    name: "com.android.net.flags-aconfig-java-export",
+    aconfig_declarations: "com.android.net.flags-aconfig",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+    min_sdk_version: "30",
+    apex_available: [
+        "com.android.wifi",
+    ],
+    mode: "exported",
+}
+
 aconfig_declarations {
     name: "com.android.net.thread.flags-aconfig",
     package: "com.android.net.thread.flags",
diff --git a/common/flags.aconfig b/common/flags.aconfig
index 17ef94b..51b4fc0 100644
--- a/common/flags.aconfig
+++ b/common/flags.aconfig
@@ -156,3 +156,21 @@
   bug: "354619988"
   is_fixed_read_only: true
 }
+
+flag {
+  name: "ipv6_over_ble"
+  is_exported: true
+  namespace: "android_core_networking"
+  description: "API flag for IPv6 over BLE"
+  bug: "372936361"
+  is_fixed_read_only: true
+}
+
+flag {
+  name: "restrict_local_network"
+  is_exported: true
+  namespace: "android_core_networking"
+  description: "Flag for controlling access to the local network behind a new runtime permission. Requires ConnectivityCompatChanges.RESTRICT_LOCAL_NETWORK to enable feature."
+  bug: "388774939"
+  is_fixed_read_only: true
+}
diff --git a/common/src/com/android/net/module/util/bpf/LocalNetAccessKey.java b/common/src/com/android/net/module/util/bpf/LocalNetAccessKey.java
new file mode 100644
index 0000000..95265b9
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/LocalNetAccessKey.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.bpf;
+
+import com.android.net.module.util.InetAddressUtils;
+import com.android.net.module.util.Struct;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+
+public class LocalNetAccessKey extends Struct {
+
+    @Field(order = 0, type = Type.U32)
+    public final long lpmBitlen;
+    @Field(order = 1, type = Type.U32)
+    public final long ifIndex;
+    @Field(order = 2, type = Type.Ipv6Address)
+    public final Inet6Address remoteAddress;
+    @Field(order = 3, type = Type.U16)
+    public final int protocol;
+    @Field(order = 4, type = Type.UBE16)
+    public final int remotePort;
+
+    public LocalNetAccessKey(long lpmBitlen, long ifIndex, InetAddress remoteAddress, int protocol,
+            int remotePort) {
+        this.lpmBitlen = lpmBitlen;
+        this.ifIndex = ifIndex;
+        this.protocol = protocol;
+        this.remotePort = remotePort;
+
+        if (remoteAddress instanceof Inet4Address) {
+            this.remoteAddress = InetAddressUtils.v4MappedV6Address((Inet4Address) remoteAddress);
+        } else {
+            this.remoteAddress = (Inet6Address) remoteAddress;
+        }
+    }
+
+    public LocalNetAccessKey(long lpmBitlen, long ifIndex, Inet6Address remoteAddress, int protocol,
+            int remotePort) {
+        this.lpmBitlen = lpmBitlen;
+        this.ifIndex = ifIndex;
+        this.remoteAddress = remoteAddress;
+        this.protocol = protocol;
+        this.remotePort = remotePort;
+    }
+
+    @Override
+    public String toString() {
+        return "LocalNetAccessKey{"
+                + "lpmBitlen=" + lpmBitlen
+                + ", ifIndex=" + ifIndex
+                + ", remoteAddress=" + remoteAddress
+                + ", protocol=" + protocol
+                + ", remotePort=" + remotePort
+                + "}";
+    }
+}
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index 26fc145..9d6d356 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -102,6 +102,7 @@
 java_library {
     name: "framework-connectivity-t-pre-jarjar",
     defaults: ["framework-connectivity-t-defaults"],
+    installable: false,
     libs: [
         "framework-bluetooth.stubs.module_lib",
         "framework-wifi.stubs.module_lib",
diff --git a/framework-t/api/current.txt b/framework-t/api/current.txt
index 9ae0cf7..d66482c 100644
--- a/framework-t/api/current.txt
+++ b/framework-t/api/current.txt
@@ -210,6 +210,23 @@
 
 package android.net.nsd {
 
+  @FlaggedApi("com.android.net.flags.ipv6_over_ble") public final class AdvertisingRequest implements android.os.Parcelable {
+    method public int describeContents();
+    method public long getFlags();
+    method public int getProtocolType();
+    method @NonNull public android.net.nsd.NsdServiceInfo getServiceInfo();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.nsd.AdvertisingRequest> CREATOR;
+    field public static final long FLAG_SKIP_PROBING = 2L; // 0x2L
+  }
+
+  @FlaggedApi("com.android.net.flags.ipv6_over_ble") public static final class AdvertisingRequest.Builder {
+    ctor public AdvertisingRequest.Builder(@NonNull android.net.nsd.NsdServiceInfo);
+    method @NonNull public android.net.nsd.AdvertisingRequest build();
+    method @NonNull public android.net.nsd.AdvertisingRequest.Builder setFlags(long);
+    method @NonNull public android.net.nsd.AdvertisingRequest.Builder setProtocolType(int);
+  }
+
   @FlaggedApi("com.android.net.flags.nsd_subtypes_support_enabled") public final class DiscoveryRequest implements android.os.Parcelable {
     method public int describeContents();
     method @Nullable public android.net.Network getNetwork();
@@ -288,6 +305,7 @@
     method public java.util.Map<java.lang.String,byte[]> getAttributes();
     method @Deprecated public java.net.InetAddress getHost();
     method @NonNull public java.util.List<java.net.InetAddress> getHostAddresses();
+    method @FlaggedApi("com.android.net.flags.ipv6_over_ble") @Nullable public String getHostname();
     method @Nullable public android.net.Network getNetwork();
     method public int getPort();
     method public String getServiceName();
diff --git a/framework-t/src/android/net/NetworkStatsAccess.java b/framework-t/src/android/net/NetworkStatsAccess.java
index 7c9b3ec..449588a 100644
--- a/framework-t/src/android/net/NetworkStatsAccess.java
+++ b/framework-t/src/android/net/NetworkStatsAccess.java
@@ -111,6 +111,12 @@
     /** Returns the {@link NetworkStatsAccess.Level} for the given caller. */
     public static @NetworkStatsAccess.Level int checkAccessLevel(
             Context context, int callingPid, int callingUid, @Nullable String callingPackage) {
+        final int appId = UserHandle.getAppId(callingUid);
+        if (appId == Process.SYSTEM_UID) {
+            // the system can access data usage for all apps on the device.
+            // check system uid first, to avoid possible dead lock from other APIs
+            return NetworkStatsAccess.Level.DEVICE;
+        }
         final DevicePolicyManager mDpm = context.getSystemService(DevicePolicyManager.class);
         final TelephonyManager tm = (TelephonyManager)
                 context.getSystemService(Context.TELEPHONY_SERVICE);
@@ -126,16 +132,13 @@
             Binder.restoreCallingIdentity(token);
         }
 
-        final int appId = UserHandle.getAppId(callingUid);
-
         final boolean isNetworkStack = PermissionUtils.hasAnyPermissionOf(
                 context, callingPid, callingUid, android.Manifest.permission.NETWORK_STACK,
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
 
-        if (hasCarrierPrivileges || isDeviceOwner
-                || appId == Process.SYSTEM_UID || isNetworkStack) {
-            // Carrier-privileged apps and device owners, and the system (including the
-            // network stack) can access data usage for all apps on the device.
+        if (hasCarrierPrivileges || isDeviceOwner || isNetworkStack) {
+            // Carrier-privileged apps and device owners, and the network stack
+            // can access data usage for all apps on the device.
             return NetworkStatsAccess.Level.DEVICE;
         }
 
diff --git a/framework-t/src/android/net/TrafficStats.java b/framework-t/src/android/net/TrafficStats.java
index 81f2cf9..868033a 100644
--- a/framework-t/src/android/net/TrafficStats.java
+++ b/framework-t/src/android/net/TrafficStats.java
@@ -17,6 +17,7 @@
 package android.net;
 
 import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+import static android.net.NetworkStats.UID_ALL;
 
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
 
@@ -33,21 +34,25 @@
 import android.content.Context;
 import android.media.MediaPlayer;
 import android.net.netstats.StatsResult;
+import android.net.netstats.TrafficStatsRateLimitCacheConfig;
 import android.os.Binder;
 import android.os.Build;
 import android.os.RemoteException;
 import android.os.StrictMode;
+import android.os.SystemClock;
 import android.util.Log;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.BinderUtils;
+import com.android.net.module.util.LruCacheWithExpiry;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
 import java.net.DatagramSocket;
 import java.net.Socket;
 import java.net.SocketException;
-
+import java.util.function.LongSupplier;
 
 /**
  * Class that provides network traffic statistics. These statistics include
@@ -182,13 +187,48 @@
     /** @hide */
     public static final int TAG_SYSTEM_PROBE = 0xFFFFFF42;
 
+    private static final StatsResult EMPTY_STATS = new StatsResult(0L, 0L, 0L, 0L);
+
+    private static final Object sRateLimitCacheLock = new Object();
+
     @GuardedBy("TrafficStats.class")
+    @Nullable
     private static INetworkStatsService sStatsService;
 
     // The variable will only be accessed in the test, which is effectively
     // single-threaded.
+    @Nullable
     private static INetworkStatsService sStatsServiceForTest = null;
 
+    // This holds the configuration for the TrafficStats rate limit caches.
+    // It will be filled with the result of a query to the service the first time
+    // the caller invokes get*Stats APIs.
+    // This variable can be accessed from any thread with the lock held.
+    @GuardedBy("sRateLimitCacheLock")
+    @Nullable
+    private static TrafficStatsRateLimitCacheConfig sRateLimitCacheConfig;
+
+    // Cache for getIfaceStats and getTotalStats binder interfaces.
+    // This variable can be accessed from any thread with the lock held,
+    // while the cache itself is thread-safe and can be accessed outside
+    // the lock.
+    @GuardedBy("sRateLimitCacheLock")
+    @Nullable
+    private static LruCacheWithExpiry<String, StatsResult> sRateLimitIfaceCache;
+
+    // Cache for getUidStats binder interface.
+    // This variable can be accessed from any thread with the lock held,
+    // while the cache itself is thread-safe and can be accessed outside
+    // the lock.
+    @GuardedBy("sRateLimitCacheLock")
+    @Nullable
+    private static LruCacheWithExpiry<Integer, StatsResult> sRateLimitUidCache;
+
+    // The variable will only be accessed in the test, which is effectively
+    // single-threaded.
+    @Nullable
+    private static LongSupplier sTimeSupplierForTest = null;
+
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
     private synchronized static INetworkStatsService getStatsService() {
         if (sStatsServiceForTest != null) return sStatsServiceForTest;
@@ -215,6 +255,28 @@
     }
 
     /**
+     * Set time supplier for test, or null to reset.
+     *
+     * @hide
+     */
+    @VisibleForTesting(visibility = PRIVATE)
+    public static void setTimeSupplierForTest(LongSupplier timeSupplier) {
+        sTimeSupplierForTest = timeSupplier;
+    }
+
+    /**
+     * Trigger query rate-limit cache config and initializing the caches.
+     *
+     * This is for test purpose.
+     *
+     * @hide
+     */
+    @VisibleForTesting(visibility = PRIVATE)
+    public static void reinitRateLimitCacheForTest() {
+        maybeGetConfigAndInitRateLimitCache(true /* forceReinit */);
+    }
+
+    /**
      * Snapshot of {@link NetworkStats} when the currently active profiling
      * session started, or {@code null} if no session active.
      *
@@ -254,6 +316,92 @@
         sStatsService = statsManager.getBinder();
     }
 
+    @Nullable
+    private static LruCacheWithExpiry<String, StatsResult> maybeGetRateLimitIfaceCache() {
+        if (!maybeGetConfigAndInitRateLimitCache(false /* forceReinit */)) return null;
+        synchronized (sRateLimitCacheLock) {
+            return sRateLimitIfaceCache;
+        }
+    }
+
+    @Nullable
+    private static LruCacheWithExpiry<Integer, StatsResult> maybeGetRateLimitUidCache() {
+        if (!maybeGetConfigAndInitRateLimitCache(false /* forceReinit */)) return null;
+        synchronized (sRateLimitCacheLock) {
+            return sRateLimitUidCache;
+        }
+    }
+
+    /**
+     * Gets the rate limit cache configuration and init caches if null.
+     *
+     * Gets the configuration from the service as the configuration
+     * is not expected to change dynamically. And use it to initialize
+     * rate-limit cache if not yet initialized.
+     *
+     * @return whether the rate-limit cache is enabled.
+     *
+     * @hide
+     */
+    private static boolean maybeGetConfigAndInitRateLimitCache(boolean forceReinit) {
+        // Access the service outside the lock to avoid potential deadlocks. This is
+        // especially important when the caller is a system component (e.g.,
+        // NetworkPolicyManagerService) that might hold other locks that the service
+        // also needs.
+        // Although this introduces a race condition where multiple threads might
+        // query the service concurrently, it's acceptable in this case because the
+        // configuration doesn't change dynamically. The configuration only needs to
+        // be fetched once before initializing the cache.
+        synchronized (sRateLimitCacheLock) {
+            if (sRateLimitCacheConfig != null && !forceReinit) {
+                return sRateLimitCacheConfig.isCacheEnabled;
+            }
+        }
+
+        final TrafficStatsRateLimitCacheConfig config;
+        try {
+            config = getStatsService().getRateLimitCacheConfig();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+
+        synchronized (sRateLimitCacheLock) {
+            if (sRateLimitCacheConfig == null || forceReinit) {
+                sRateLimitCacheConfig = config;
+                initRateLimitCacheLocked();
+            }
+        }
+        return config.isCacheEnabled;
+    }
+
+    @GuardedBy("sRateLimitCacheLock")
+    private static void initRateLimitCacheLocked() {
+        // Set up rate limiting caches.
+        // Use uid cache with UID_ALL to cache total stats.
+        if (sRateLimitCacheConfig.isCacheEnabled) {
+            // A time supplier which is monotonic until device reboots, and counts
+            // time spent in sleep. This is needed to ensure the get*Stats caller
+            // won't get stale value after system time adjustment or waking up from sleep.
+            final LongSupplier realtimeSupplier = (sTimeSupplierForTest != null
+                    ? sTimeSupplierForTest : () -> SystemClock.elapsedRealtime());
+            sRateLimitIfaceCache = new LruCacheWithExpiry<String, StatsResult>(
+                    realtimeSupplier,
+                    sRateLimitCacheConfig.expiryDurationMs,
+                    sRateLimitCacheConfig.maxEntries,
+                    (statsResult) -> !isEmpty(statsResult)
+            );
+            sRateLimitUidCache = new LruCacheWithExpiry<Integer, StatsResult>(
+                    realtimeSupplier,
+                    sRateLimitCacheConfig.expiryDurationMs,
+                    sRateLimitCacheConfig.maxEntries,
+                    (statsResult) -> !isEmpty(statsResult)
+            );
+        } else {
+            sRateLimitIfaceCache = null;
+            sRateLimitUidCache = null;
+        }
+    }
+
     /**
      * Attach the socket tagger implementation to the current process, to
      * get notified when a socket's {@link FileDescriptor} is assigned to
@@ -736,6 +884,14 @@
             android.Manifest.permission.NETWORK_STACK,
             android.Manifest.permission.NETWORK_SETTINGS})
     public static void clearRateLimitCaches() {
+        final LruCacheWithExpiry<String, StatsResult> ifaceCache = maybeGetRateLimitIfaceCache();
+        if (ifaceCache != null) {
+            ifaceCache.clear();
+        }
+        final LruCacheWithExpiry<Integer, StatsResult> uidCache = maybeGetRateLimitUidCache();
+        if (uidCache != null) {
+            uidCache.clear();
+        }
         try {
             getStatsService().clearTrafficStatsRateLimitCaches();
         } catch (RemoteException e) {
@@ -985,35 +1141,76 @@
 
     /** @hide */
     public static long getUidStats(int uid, int type) {
-        final StatsResult stats;
+        return fetchStats(maybeGetRateLimitUidCache(), uid,
+                () -> getStatsService().getUidStats(uid), type);
+    }
+
+    // Note: This method calls to the service, do not invoke this method with lock held.
+    private static <K> long fetchStats(
+            @Nullable LruCacheWithExpiry<K, StatsResult> cache, K key,
+            BinderUtils.ThrowingSupplier<StatsResult, RemoteException> statsFetcher, int type) {
         try {
-            stats = getStatsService().getUidStats(uid);
+            final StatsResult stats;
+            if (cache != null) {
+                stats = fetchStatsWithCache(cache, key, statsFetcher);
+            } else {
+                // Cache is not enabled, fetch directly from service.
+                stats = statsFetcher.get();
+            }
+            return getEntryValueForType(stats, type);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
-        return getEntryValueForType(stats, type);
+    }
+
+    // Note: This method calls to the service, do not invoke this method with lock held.
+    @Nullable
+    private static <K> StatsResult fetchStatsWithCache(LruCacheWithExpiry<K, StatsResult> cache,
+            K key, BinderUtils.ThrowingSupplier<StatsResult, RemoteException> statsFetcher)
+            throws RemoteException {
+        // Attempt to retrieve from the cache first.
+        StatsResult stats = cache.get(key);
+
+        // Although the cache instance is thread-safe, this can still introduce a
+        // race condition between threads of the same process, potentially
+        // returning non-monotonic results. This is because there is no lock
+        // between get, fetch, and put operations. This is considered acceptable
+        // because varying thread execution speeds can also cause non-monotonic
+        // results, even with locking.
+        if (stats == null) {
+            // Cache miss, fetch from the service.
+            stats = statsFetcher.get();
+
+            // Update the cache with the fetched result if valid.
+            if (stats != null && !isEmpty(stats)) {
+                final StatsResult cachedValue = cache.putIfAbsent(key, stats);
+                if (cachedValue != null) {
+                    // Some other thread cached a value after this thread
+                    // originally got a cache miss. Return the cached value
+                    // to ensure all returned values after caching are consistent.
+                    return cachedValue;
+                }
+            }
+        }
+        return stats;
+    }
+
+    private static boolean isEmpty(StatsResult stats) {
+        return stats.equals(EMPTY_STATS);
     }
 
     /** @hide */
     public static long getTotalStats(int type) {
-        final StatsResult stats;
-        try {
-            stats = getStatsService().getTotalStats();
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-        return getEntryValueForType(stats, type);
+        // In practice, Bpf doesn't use UID_ALL for storing per-UID stats.
+        // Use uid cache with UID_ALL to cache total stats.
+        return fetchStats(maybeGetRateLimitUidCache(), UID_ALL,
+                () -> getStatsService().getTotalStats(), type);
     }
 
     /** @hide */
     public static long getIfaceStats(String iface, int type) {
-        final StatsResult stats;
-        try {
-            stats = getStatsService().getIfaceStats(iface);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-        return getEntryValueForType(stats, type);
+        return fetchStats(maybeGetRateLimitIfaceCache(), iface,
+                () -> getStatsService().getIfaceStats(iface), type);
     }
 
     /**
diff --git a/framework-t/src/android/net/nsd/AdvertisingRequest.java b/framework-t/src/android/net/nsd/AdvertisingRequest.java
index 6afb2d5..a62df65 100644
--- a/framework-t/src/android/net/nsd/AdvertisingRequest.java
+++ b/framework-t/src/android/net/nsd/AdvertisingRequest.java
@@ -15,12 +15,16 @@
  */
 package android.net.nsd;
 
+import android.annotation.FlaggedApi;
 import android.annotation.LongDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.net.nsd.NsdManager.ProtocolType;
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import com.android.net.flags.Flags;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.time.Duration;
@@ -28,16 +32,32 @@
 
 /**
  * Encapsulates parameters for {@link NsdManager#registerService}.
- * @hide
  */
-//@FlaggedApi(NsdManager.Flags.ADVERTISE_REQUEST_API)
+@FlaggedApi(Flags.FLAG_IPV6_OVER_BLE)
 public final class AdvertisingRequest implements Parcelable {
 
     /**
      * Only update the registration without sending exit and re-announcement.
+     * @hide
      */
     public static final long NSD_ADVERTISING_UPDATE_ONLY = 1;
 
+    // TODO: if apps are allowed to set hostnames, the below doc should be updated to mention that
+    // passed in hostnames must also be known unique to use this flag.
+    /**
+     * Skip the probing step when advertising.
+     *
+     * <p>This must only be used when the service name ({@link NsdServiceInfo#getServiceName()} is
+     * known to be unique and cannot possibly be used by any other device on the network.
+     */
+    public static final long FLAG_SKIP_PROBING = 1 << 1;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @LongDef(flag = true, prefix = {"FLAG_"}, value = {
+            FLAG_SKIP_PROBING,
+    })
+    public @interface AdvertisingFlags {}
 
     @NonNull
     public static final Creator<AdvertisingRequest> CREATOR =
@@ -79,7 +99,7 @@
     /**
      * The constructor for the advertiseRequest
      */
-    private AdvertisingRequest(@NonNull NsdServiceInfo serviceInfo, int protocolType,
+    private AdvertisingRequest(@NonNull NsdServiceInfo serviceInfo, @ProtocolType int protocolType,
             long advertisingConfig, @NonNull Duration ttl) {
         mServiceInfo = serviceInfo;
         mProtocolType = protocolType;
@@ -88,7 +108,7 @@
     }
 
     /**
-     * Returns the {@link NsdServiceInfo}
+     * @return the {@link NsdServiceInfo} describing the service to advertise.
      */
     @NonNull
     public NsdServiceInfo getServiceInfo() {
@@ -96,16 +116,18 @@
     }
 
     /**
-     * Returns the service advertise protocol
+     * @return the service advertisement protocol.
      */
+    @ProtocolType
     public int getProtocolType() {
         return mProtocolType;
     }
 
     /**
-     * Returns the advertising config.
+     * @return the flags affecting advertising behavior.
      */
-    public long getAdvertisingConfig() {
+    @AdvertisingFlags
+    public long getFlags() {
         return mAdvertisingConfig;
     }
 
@@ -165,34 +187,45 @@
         dest.writeLong(mTtl == null ? -1L : mTtl.getSeconds());
     }
 
-//    @FlaggedApi(NsdManager.Flags.ADVERTISE_REQUEST_API)
     /**
-     * The builder for creating new {@link AdvertisingRequest} objects.
-     * @hide
+     * A builder for creating new {@link AdvertisingRequest} objects.
      */
+    @FlaggedApi(Flags.FLAG_IPV6_OVER_BLE)
     public static final class Builder {
         @NonNull
         private final NsdServiceInfo mServiceInfo;
-        private final int mProtocolType;
+        private int mProtocolType;
         private long mAdvertisingConfig;
         @Nullable
         private Duration mTtl;
+
         /**
          * Creates a new {@link Builder} object.
+         * @param serviceInfo the {@link NsdServiceInfo} describing the service to advertise.
+         * @param protocolType the advertising protocol to use.
+         * @hide
          */
-        public Builder(@NonNull NsdServiceInfo serviceInfo, int protocolType) {
+        public Builder(@NonNull NsdServiceInfo serviceInfo, @ProtocolType int protocolType) {
             mServiceInfo = serviceInfo;
             mProtocolType = protocolType;
         }
 
         /**
+         * Creates a new {@link Builder} object.
+         * @param serviceInfo the {@link NsdServiceInfo} describing the service to advertise.
+         */
+        public Builder(@NonNull NsdServiceInfo serviceInfo) {
+            this(serviceInfo, NsdManager.PROTOCOL_DNS_SD);
+        }
+
+        /**
          * Sets advertising configuration flags.
          *
-         * @param advertisingConfigFlags Bitmask of {@code AdvertisingConfig} flags.
+         * @param flags flags to use for advertising.
          */
         @NonNull
-        public Builder setAdvertisingConfig(long advertisingConfigFlags) {
-            mAdvertisingConfig = advertisingConfigFlags;
+        public Builder setFlags(@AdvertisingFlags long flags) {
+            mAdvertisingConfig = flags;
             return this;
         }
 
@@ -232,6 +265,16 @@
             return this;
         }
 
+        /**
+         * Sets the protocol to use for advertising.
+         * @param protocolType the advertising protocol to use.
+         */
+        @NonNull
+        public Builder setProtocolType(@ProtocolType int protocolType) {
+            mProtocolType = protocolType;
+            return this;
+        }
+
         /** Creates a new {@link AdvertisingRequest} object. */
         @NonNull
         public AdvertisingRequest build() {
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index 116bea6..426a92d 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -314,6 +314,13 @@
     /** Dns based service discovery protocol */
     public static final int PROTOCOL_DNS_SD = 0x0001;
 
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = {"PROTOCOL_"}, value = {
+            PROTOCOL_DNS_SD,
+    })
+    public @interface ProtocolType {}
+
     /**
      * The minimum TTL seconds which is allowed for a service registration.
      *
@@ -1272,7 +1279,7 @@
         // documented in the NsdServiceInfo.setSubtypes API instead, but this provides a limited
         // option for users of the older undocumented behavior, only for subtype changes.
         if (isSubtypeUpdateRequest(serviceInfo, listener)) {
-            builder.setAdvertisingConfig(AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY);
+            builder.setFlags(AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY);
         }
         registerService(builder.build(), executor, listener);
     }
@@ -1358,7 +1365,7 @@
         checkProtocol(protocolType);
         final int key;
         // For update only request, the old listener has to be reused
-        if ((advertisingRequest.getAdvertisingConfig()
+        if ((advertisingRequest.getFlags()
                 & AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY) > 0) {
             key = updateRegisteredListener(listener, executor, serviceInfo);
         } else {
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
index 18c59d9..6a5ab4d 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -200,19 +200,19 @@
     /**
      * Get the hostname.
      *
-     * <p>When a service is resolved, it returns the hostname of the resolved service . The top
-     * level domain ".local." is omitted.
-     *
-     * <p>For example, it returns "MyHost" when the service's hostname is "MyHost.local.".
-     *
-     * @hide
+     * <p>When a service is resolved through {@link NsdManager#resolveService} or
+     * {@link NsdManager#registerServiceInfoCallback}, this returns the hostname of the resolved
+     * service. In all other cases, this will be null. The top level domain ".local." is omitted.
+     * For example, this returns "MyHost" when the service's hostname is "MyHost.local.".
      */
-//    @FlaggedApi(NsdManager.Flags.NSD_CUSTOM_HOSTNAME_ENABLED)
+    @FlaggedApi(Flags.FLAG_IPV6_OVER_BLE)
     @Nullable
     public String getHostname() {
         return mHostname;
     }
 
+    // TODO: if setHostname is made public, AdvertisingRequest#FLAG_SKIP_PROBING javadoc must be
+    // updated to mention that hostnames must also be known unique to use that flag.
     /**
      * Set a custom hostname for this service instance for registration.
      *
diff --git a/framework-t/src/android/net/nsd/OffloadServiceInfo.java b/framework-t/src/android/net/nsd/OffloadServiceInfo.java
index e4b2f43..fd824f3 100644
--- a/framework-t/src/android/net/nsd/OffloadServiceInfo.java
+++ b/framework-t/src/android/net/nsd/OffloadServiceInfo.java
@@ -282,7 +282,7 @@
         }
 
         /**
-         * Get the service type. (e.g. "_http._tcp.local" )
+         * Get the service type. (e.g. "_http._tcp" )
          */
         @NonNull
         public String getServiceType() {
diff --git a/framework/Android.bp b/framework/Android.bp
index a93a532..f66bc60 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -75,6 +75,7 @@
             // the module builds against API (the parcelable declarations exist in framework.aidl)
             "frameworks/base/core/java", // For framework parcelables
             "frameworks/native/aidl/binder", // For PersistableBundle.aidl
+            "packages/modules/Connectivity/Tethering/common/TetheringLib/src",
         ],
     },
     stub_only_libs: [
@@ -143,6 +144,7 @@
 java_library {
     name: "framework-connectivity-pre-jarjar",
     defaults: ["framework-module-defaults"],
+    installable: false,
     min_sdk_version: "30",
     static_libs: [
         "framework-connectivity-pre-jarjar-without-cronet",
@@ -158,7 +160,9 @@
 java_defaults {
     name: "CronetJavaDefaults",
     srcs: [":httpclient_api_sources"],
-    static_libs: ["com.android.net.http.flags-aconfig-java"],
+    static_libs: [
+        "com.android.net.http.flags-aconfig-java",
+    ],
     libs: [
         "androidx.annotation_annotation",
     ],
@@ -334,6 +338,7 @@
     aidl: {
         include_dirs: [
             "packages/modules/Connectivity/framework/aidl-export",
+            "packages/modules/Connectivity/Tethering/common/TetheringLib/src",
             "frameworks/native/aidl/binder", // For PersistableBundle.aidl
         ],
     },
diff --git a/framework/api/current.txt b/framework/api/current.txt
index 7bc0cf3..323c533 100644
--- a/framework/api/current.txt
+++ b/framework/api/current.txt
@@ -103,6 +103,7 @@
     method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, int);
     method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler, int);
     method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.app.PendingIntent);
+    method @FlaggedApi("com.android.net.flags.ipv6_over_ble") public void reserveNetwork(@NonNull android.net.NetworkRequest, @NonNull android.os.Handler, @NonNull android.net.ConnectivityManager.NetworkCallback);
     method @Deprecated public void setNetworkPreference(int);
     method @Deprecated public static boolean setProcessDefaultNetwork(@Nullable android.net.Network);
     method public void unregisterNetworkCallback(@NonNull android.net.ConnectivityManager.NetworkCallback);
@@ -151,6 +152,7 @@
     method public void onLinkPropertiesChanged(@NonNull android.net.Network, @NonNull android.net.LinkProperties);
     method public void onLosing(@NonNull android.net.Network, int);
     method public void onLost(@NonNull android.net.Network);
+    method @FlaggedApi("com.android.net.flags.ipv6_over_ble") public void onReserved(@NonNull android.net.NetworkCapabilities);
     method public void onUnavailable();
     field public static final int FLAG_INCLUDE_LOCATION_INFO = 1; // 0x1
   }
@@ -231,6 +233,32 @@
     field @NonNull public static final android.os.Parcelable.Creator<android.net.IpPrefix> CREATOR;
   }
 
+  @FlaggedApi("com.android.net.flags.ipv6_over_ble") public final class L2capNetworkSpecifier extends android.net.NetworkSpecifier implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getHeaderCompression();
+    method public int getPsm();
+    method @Nullable public android.net.MacAddress getRemoteAddress();
+    method public int getRole();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.L2capNetworkSpecifier> CREATOR;
+    field public static final int HEADER_COMPRESSION_6LOWPAN = 2; // 0x2
+    field public static final int HEADER_COMPRESSION_ANY = 0; // 0x0
+    field public static final int HEADER_COMPRESSION_NONE = 1; // 0x1
+    field public static final int PSM_ANY = 0; // 0x0
+    field public static final int ROLE_ANY = 0; // 0x0
+    field public static final int ROLE_CLIENT = 1; // 0x1
+    field public static final int ROLE_SERVER = 2; // 0x2
+  }
+
+  public static final class L2capNetworkSpecifier.Builder {
+    ctor public L2capNetworkSpecifier.Builder();
+    method @NonNull public android.net.L2capNetworkSpecifier build();
+    method @NonNull public android.net.L2capNetworkSpecifier.Builder setHeaderCompression(int);
+    method @NonNull public android.net.L2capNetworkSpecifier.Builder setPsm(@IntRange(from=0, to=255) int);
+    method @NonNull public android.net.L2capNetworkSpecifier.Builder setRemoteAddress(@Nullable android.net.MacAddress);
+    method @NonNull public android.net.L2capNetworkSpecifier.Builder setRole(int);
+  }
+
   public class LinkAddress implements android.os.Parcelable {
     method public int describeContents();
     method public java.net.InetAddress getAddress();
diff --git a/framework/src/android/net/BpfNetMapsConstants.java b/framework/src/android/net/BpfNetMapsConstants.java
index f3773de..f1a6f00 100644
--- a/framework/src/android/net/BpfNetMapsConstants.java
+++ b/framework/src/android/net/BpfNetMapsConstants.java
@@ -60,6 +60,11 @@
             "/sys/fs/bpf/netd_shared/map_netd_data_saver_enabled_map";
     public static final String INGRESS_DISCARD_MAP_PATH =
             "/sys/fs/bpf/netd_shared/map_netd_ingress_discard_map";
+    public static final String LOCAL_NET_ACCESS_MAP_PATH =
+            "/sys/fs/bpf/netd_shared/map_netd_local_net_access_map";
+    public static final String LOCAL_NET_BLOCKED_UID_MAP_PATH =
+            "/sys/fs/bpf/netd_shared/map_netd_local_net_blocked_uid_map";
+
     public static final Struct.S32 UID_RULES_CONFIGURATION_KEY = new Struct.S32(0);
     public static final Struct.S32 CURRENT_STATS_MAP_CONFIGURATION_KEY = new Struct.S32(1);
     public static final Struct.S32 DATA_SAVER_ENABLED_KEY = new Struct.S32(0);
diff --git a/framework/src/android/net/CaptivePortal.java b/framework/src/android/net/CaptivePortal.java
index 4a7b601..4c534f3 100644
--- a/framework/src/android/net/CaptivePortal.java
+++ b/framework/src/android/net/CaptivePortal.java
@@ -18,10 +18,19 @@
 import android.annotation.NonNull;
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
+import android.annotation.TargetApi;
+import android.os.Binder;
+import android.os.Build;
 import android.os.IBinder;
+import android.os.OutcomeReceiver;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.system.OsConstants;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
 
 /**
  * A class allowing apps handling the {@link ConnectivityManager#ACTION_CAPTIVE_PORTAL_SIGN_IN}
@@ -69,6 +78,15 @@
     @SystemApi
     public static final int APP_REQUEST_REEVALUATION_REQUIRED = APP_REQUEST_BASE + 0;
 
+    /**
+     * Binder object used for tracking the lifetime of the process, so CS can perform cleanup if
+     * the CaptivePortal app dies. This binder is not parcelled as part of this object. It is
+     * created in the client process and sent to the server by setDelegateUid so that the server
+     * can use it to register a death recipient.
+     *
+     */
+    private final Binder mLifetimeBinder = new Binder();
+
     private final IBinder mBinder;
 
     /** @hide */
@@ -167,4 +185,56 @@
     @SystemApi
     public void logEvent(int eventId, @NonNull String packageName) {
     }
+
+    /**
+     * Sets the UID of the app that is allowed to perform network traffic for captive
+     * portal login.
+     *
+     * This app will be allowed to communicate directly on the captive
+     * portal by binding to the {@link android.net.Network} extra passed in the
+     * ACTION_CAPTIVE_PORTAL_SIGN_IN broadcast that contained this object.
+     *
+     * Communication will bypass network access restrictions such as VPNs and
+     * Private DNS settings, so the delegated UID must be trusted to ensure that only
+     * traffic intended for captive portal login binds to that network.
+     *
+     * By default, no UID is delegated. The delegation can be cleared by calling
+     * this method again with {@link android.os.Process.INVALID_UID}. Only one UID can
+     * be delegated at any given time.
+     *
+     * The operation is asynchronous. The uid is only guaranteed to have access when
+     * the provided OutcomeReceiver is called.
+     *
+     * @hide
+     */
+    @RequiresPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
+    // OutcomeReceiver is not available on R, but the mainline version of this
+    // class is only available on S+.
+    @TargetApi(Build.VERSION_CODES.S)
+    public void setDelegateUid(int uid, @NonNull Executor executor,
+            @NonNull final OutcomeReceiver<Void, ServiceSpecificException> receiver) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(receiver);
+        try {
+            ICaptivePortal.Stub.asInterface(mBinder).setDelegateUid(
+                    uid,
+                    mLifetimeBinder,
+                    new IIntResultListener.Stub() {
+                        @Override
+                        public void onResult(int resultCode) {
+                            if (resultCode != 0) {
+                                final String msg = "Fail to set the delegate UID " + uid
+                                        + ", error: " + OsConstants.errnoName(resultCode);
+                                executor.execute(() -> {
+                                    receiver.onError(new ServiceSpecificException(resultCode, msg));
+                                });
+                            } else {
+                                executor.execute(() -> receiver.onResult(null));
+                            }
+                        }
+                    });
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
 }
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 1ebc4a3..5d99b74 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -21,6 +21,7 @@
 import static android.net.NetworkRequest.Type.LISTEN;
 import static android.net.NetworkRequest.Type.LISTEN_FOR_BEST;
 import static android.net.NetworkRequest.Type.REQUEST;
+import static android.net.NetworkRequest.Type.RESERVATION;
 import static android.net.NetworkRequest.Type.TRACK_DEFAULT;
 import static android.net.NetworkRequest.Type.TRACK_SYSTEM_DEFAULT;
 import static android.net.QosCallback.QosCallbackRegistrationException;
@@ -1873,7 +1874,7 @@
     public NetworkCapabilities[] getDefaultNetworkCapabilitiesForUser(int userId) {
         try {
             return mService.getDefaultNetworkCapabilitiesForUser(
-                    userId, mContext.getOpPackageName(), getAttributionTag());
+                    userId, mContext.getOpPackageName(), mContext.getAttributionTag());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1967,7 +1968,7 @@
             @NonNull String packageName) {
         try {
             return mService.getRedactedLinkPropertiesForPackage(
-                    lp, uid, packageName, getAttributionTag());
+                    lp, uid, packageName, mContext.getAttributionTag());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1993,7 +1994,7 @@
     public NetworkCapabilities getNetworkCapabilities(@Nullable Network network) {
         try {
             return mService.getNetworkCapabilities(
-                    network, mContext.getOpPackageName(), getAttributionTag());
+                    network, mContext.getOpPackageName(), mContext.getAttributionTag());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -2027,7 +2028,7 @@
             int uid, @NonNull String packageName) {
         try {
             return mService.getRedactedNetworkCapabilitiesForPackage(nc, uid, packageName,
-                    getAttributionTag());
+                    mContext.getAttributionTag());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -2752,21 +2753,13 @@
         checkLegacyRoutingApiAccess();
         try {
             return mService.requestRouteToHostAddress(networkType, hostAddress.getAddress(),
-                    mContext.getOpPackageName(), getAttributionTag());
+                    mContext.getOpPackageName(), mContext.getAttributionTag());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
     }
 
     /**
-     * @return the context's attribution tag
-     */
-    // TODO: Remove method and replace with direct call once R code is pushed to AOSP
-    private @Nullable String getAttributionTag() {
-        return mContext.getAttributionTag();
-    }
-
-    /**
      * Returns the value of the setting for background data usage. If false,
      * applications should not use the network if the application is not in the
      * foreground. Developers should respect this setting, and check the value
@@ -3072,7 +3065,8 @@
      * <p>WARNING: New clients should not use this function. The only usages should be in PanService
      * and WifiStateMachine which need direct access. All other clients should use
      * {@link #startTethering} and {@link #stopTethering} which encapsulate proper provisioning
-     * logic.</p>
+     * logic. On SDK versions after {@link Build.VERSION_CODES.VANILLA_ICE_CREAM}, this will throw
+     * an UnsupportedOperationException.</p>
      *
      * @param iface the interface name to tether.
      * @return error a {@code TETHER_ERROR} value indicating success or failure type
@@ -3097,7 +3091,8 @@
      * <p>WARNING: New clients should not use this function. The only usages should be in PanService
      * and WifiStateMachine which need direct access. All other clients should use
      * {@link #startTethering} and {@link #stopTethering} which encapsulate proper provisioning
-     * logic.</p>
+     * logic. On SDK versions after {@link Build.VERSION_CODES.VANILLA_ICE_CREAM}, this will throw
+     * an UnsupportedOperationException.</p>
      *
      * @param iface the interface name to untether.
      * @return error a {@code TETHER_ERROR} value indicating success or failure type
@@ -4279,12 +4274,18 @@
         private static final int METHOD_ONLOST = 6;
 
         /**
-         * Called if no network is found within the timeout time specified in
-         * {@link #requestNetwork(NetworkRequest, NetworkCallback, int)} call or if the
-         * requested network request cannot be fulfilled (whether or not a timeout was
-         * specified). When this callback is invoked the associated
-         * {@link NetworkRequest} will have already been removed and released, as if
-         * {@link #unregisterNetworkCallback(NetworkCallback)} had been called.
+         * If the callback was registered with one of the {@code requestNetwork} methods, this will
+         * be called if no network is found within the timeout specified in {@link
+         * #requestNetwork(NetworkRequest, NetworkCallback, int)} call or if the requested network
+         * request cannot be fulfilled (whether or not a timeout was specified).
+         *
+         * If the callback was registered when reserving a network, this method indicates that the
+         * reservation is removed. It can be called when the reservation is requested, because the
+         * system could not satisfy the reservation, or after the reserved network connects.
+         *
+         * When this callback is invoked the associated {@link NetworkRequest} will have already
+         * been removed and released, as if {@link #unregisterNetworkCallback(NetworkCallback)} had
+         * been called.
          */
         @FilteredCallback(methodId = METHOD_ONUNAVAILABLE, calledByCallbackId = CALLBACK_UNAVAIL)
         public void onUnavailable() {}
@@ -4425,6 +4426,28 @@
         }
         private static final int METHOD_ONBLOCKEDSTATUSCHANGED_INT = 14;
 
+        /**
+         * Called when a network is reserved.
+         *
+         * The reservation includes the {@link NetworkCapabilities} that uniquely describe the
+         * network that was reserved. the caller communicates this information to hardware or
+         * software components on or off-device to instruct them to create a network matching this
+         * reservation.
+         *
+         * {@link #onReserved(NetworkCapabilities)} is called at most once and is guaranteed to be
+         * called before any other callback unless the reservation is unavailable.
+         *
+         * Once a reservation is made, the reserved {@link NetworkCapabilities} will not be updated,
+         * and the reservation remains in place until the reserved network connects or {@link
+         * #onUnavailable} is called.
+         *
+         * @param networkCapabilities The {@link NetworkCapabilities} of the reservation.
+         */
+        @FlaggedApi(Flags.FLAG_IPV6_OVER_BLE)
+        @FilteredCallback(methodId = METHOD_ONRESERVED, calledByCallbackId = CALLBACK_RESERVED)
+        public void onReserved(@NonNull NetworkCapabilities networkCapabilities) {}
+        private static final int METHOD_ONRESERVED = 15;
+
         private NetworkRequest networkRequest;
         private final int mFlags;
     }
@@ -4476,6 +4499,8 @@
     public static final int CALLBACK_BLK_CHANGED                = 11;
     /** @hide */
     public static final int CALLBACK_LOCAL_NETWORK_INFO_CHANGED = 12;
+    /** @hide */
+    public static final int CALLBACK_RESERVED                   = 13;
     // When adding new IDs, note CallbackQueue assumes callback IDs are at most 16 bits.
 
 
@@ -4495,6 +4520,7 @@
             case CALLBACK_RESUMED:      return "CALLBACK_RESUMED";
             case CALLBACK_BLK_CHANGED:  return "CALLBACK_BLK_CHANGED";
             case CALLBACK_LOCAL_NETWORK_INFO_CHANGED: return "CALLBACK_LOCAL_NETWORK_INFO_CHANGED";
+            case CALLBACK_RESERVED:     return "CALLBACK_RESERVED";
             default:
                 return Integer.toString(whichCallback);
         }
@@ -4525,6 +4551,7 @@
     public static class NetworkCallbackMethodsHolder {
         public static final NetworkCallbackMethod[] NETWORK_CB_METHODS =
                 new NetworkCallbackMethod[] {
+                        method("onReserved", 1 << CALLBACK_RESERVED, NetworkCapabilities.class),
                         method("onPreCheck", 1 << CALLBACK_PRECHECK, Network.class),
                         // Note the final overload of onAvailable is not included, since it cannot
                         // match any overridden method.
@@ -4604,6 +4631,11 @@
             }
 
             switch (message.what) {
+                case CALLBACK_RESERVED: {
+                    final NetworkCapabilities cap = getObject(message, NetworkCapabilities.class);
+                    callback.onReserved(cap);
+                    break;
+                }
                 case CALLBACK_PRECHECK: {
                     callback.onPreCheck(network);
                     break;
@@ -4705,12 +4737,12 @@
                 if (reqType == LISTEN) {
                     request = mService.listenForNetwork(
                             need, messenger, binder, callbackFlags, callingPackageName,
-                            getAttributionTag(), declaredMethodsFlag);
+                            mContext.getAttributionTag(), declaredMethodsFlag);
                 } else {
                     request = mService.requestNetwork(
                             asUid, need, reqType.ordinal(), messenger, timeoutMs, binder,
-                            legacyType, callbackFlags, callingPackageName, getAttributionTag(),
-                            declaredMethodsFlag);
+                            legacyType, callbackFlags, callingPackageName,
+                            mContext.getAttributionTag(), declaredMethodsFlag);
                 }
                 if (request != null) {
                     sCallbacks.put(request, callback);
@@ -4985,6 +5017,41 @@
     }
 
     /**
+     * Reserve a network to satisfy a set of {@link NetworkCapabilities}.
+     *
+     * Some types of networks require the system to generate (i.e. reserve) some set of information
+     * before a network can be connected. For such networks, {@link #reserveNetwork} can be used
+     * which may lead to a call to {@link NetworkCallback#onReserved(NetworkCapabilities)}
+     * containing the {@link NetworkCapabilities} that were reserved.
+     *
+     * A reservation reserves at most one network. If the network connects, a reservation request
+     * behaves similar to a request filed using {@link #requestNetwork}. The provided {@link
+     * NetworkCallback} will only be called for the reserved network.
+     *
+     * If the system determines that the requested reservation can never be fulfilled, {@link
+     * NetworkCallback#onUnavailable} is called, the reservation is released by the system, and the
+     * provided callback can be reused. Otherwise, the reservation remains in place until the
+     * requested network connects. There is no guarantee that the reserved network will ever
+     * connect.
+     *
+     * @param request {@link NetworkRequest} describing this request.
+     * @param handler {@link Handler} to specify the thread upon which the callback will be invoked.
+     * @param networkCallback The {@link NetworkCallback} to be utilized for this request. Note
+     *                        the callback must not be shared - it uniquely specifies this request.
+     */
+    // TODO: add executor overloads for all network request methods. Any method that passed an
+    // Executor could process the messages on the singleton ConnectivityThread Handler.
+    @SuppressLint("ExecutorRegistration")
+    @FlaggedApi(Flags.FLAG_IPV6_OVER_BLE)
+    public void reserveNetwork(@NonNull NetworkRequest request,
+            @NonNull Handler handler,
+            @NonNull NetworkCallback networkCallback) {
+        final CallbackHandler cbHandler = new CallbackHandler(handler);
+        final NetworkCapabilities nc = request.networkCapabilities;
+        sendRequestForNetwork(nc, networkCallback, 0, RESERVATION, TYPE_NONE, cbHandler);
+    }
+
+    /**
      * Request a network to satisfy a set of {@link NetworkCapabilities}, limited
      * by a timeout.
      *
@@ -5127,7 +5194,7 @@
         try {
             mService.pendingRequestForNetwork(
                     request.networkCapabilities, operation, mContext.getOpPackageName(),
-                    getAttributionTag());
+                    mContext.getAttributionTag());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         } catch (ServiceSpecificException e) {
@@ -5276,7 +5343,7 @@
         try {
             mService.pendingListenForNetwork(
                     request.networkCapabilities, operation, mContext.getOpPackageName(),
-                    getAttributionTag());
+                    mContext.getAttributionTag());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         } catch (ServiceSpecificException e) {
diff --git a/framework/src/android/net/ICaptivePortal.aidl b/framework/src/android/net/ICaptivePortal.aidl
index e35f8d4..5cbb428 100644
--- a/framework/src/android/net/ICaptivePortal.aidl
+++ b/framework/src/android/net/ICaptivePortal.aidl
@@ -16,6 +16,9 @@
 
 package android.net;
 
+import android.net.IIntResultListener;
+import android.os.IBinder;
+
 /**
  * Interface to inform NetworkMonitor of decisions of app handling captive portal.
  * @hide
@@ -23,4 +26,5 @@
 oneway interface ICaptivePortal {
     void appRequest(int request);
     void appResponse(int response);
+    void setDelegateUid(int uid, IBinder binder, IIntResultListener listener);
 }
diff --git a/framework/src/android/net/L2capNetworkSpecifier.java b/framework/src/android/net/L2capNetworkSpecifier.java
new file mode 100644
index 0000000..93f9352
--- /dev/null
+++ b/framework/src/android/net/L2capNetworkSpecifier.java
@@ -0,0 +1,415 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.net.flags.Flags;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * A {@link NetworkSpecifier} used to identify an L2CAP network over BLE.
+ *
+ * An L2CAP network is not symmetrical, meaning there exists both a server (Bluetooth peripheral)
+ * and a client (Bluetooth central) node. This specifier contains the information required to
+ * request a client L2CAP network using {@link ConnectivityManager#requestNetwork} while specifying
+ * the remote MAC address, and Protocol/Service Multiplexer (PSM). It can also contain information
+ * allocated by the system when reserving a server network using {@link
+ * ConnectivityManager#reserveNetwork} such as the Protocol/Service Multiplexer (PSM). In both
+ * cases, the header compression option must be specified.
+ *
+ * An L2CAP server network allocates a Protocol/Service Multiplexer (PSM) to be advertised to the
+ * client. A new server network must always be reserved using {@code
+ * ConnectivityManager#reserveNetwork}. The subsequent {@link
+ * ConnectivityManager.NetworkCallback#onReserved(NetworkCapabilities)} callback includes an {@code
+ * L2CapNetworkSpecifier}. The {@link getPsm()} method will return the Protocol/Service Multiplexer
+ * (PSM) of the reserved network so that the server can advertise it to the client and the client
+ * can connect.
+ * An L2CAP server network is backed by a {@link android.bluetooth.BluetoothServerSocket} which can,
+ * in theory, accept many connections. However, before SDK version {@link
+ * Build.VERSION_CODES.VANILLA_ICE_CREAM} Bluetooth APIs do not expose the channel ID, so these
+ * connections are indistinguishable. In practice, this means that the network matching semantics in
+ * ConnectivityService will tear down all but the first connection.
+ *
+ * When the connection between client and server completes, a {@link Network} whose capabilities
+ * satisfy this {@code L2capNetworkSpecifier} will connect and the usual callbacks, such as {@link
+ * NetworkCallback#onAvailable}, will be called on the callback object passed to {@code
+ * ConnectivityManager#reserveNetwork} or {@code ConnectivityManager#requestNetwork}.
+ */
+@FlaggedApi(Flags.FLAG_IPV6_OVER_BLE)
+public final class L2capNetworkSpecifier extends NetworkSpecifier implements Parcelable {
+    /**
+     * Match any role.
+     *
+     * This role is only meaningful in {@link NetworkRequest}s. Specifiers for actual L2CAP
+     * networks never have this role set.
+     */
+    public static final int ROLE_ANY = 0;
+    /** Specifier describes a client network, i.e., the device is the Bluetooth central. */
+    public static final int ROLE_CLIENT = 1;
+    /** Specifier describes a server network, i.e., the device is the Bluetooth peripheral. */
+    public static final int ROLE_SERVER = 2;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(flag = false, prefix = "ROLE_", value = {
+        ROLE_ANY,
+        ROLE_CLIENT,
+        ROLE_SERVER
+    })
+    public @interface Role {}
+    /** Role used to distinguish client from server networks. */
+    @Role
+    private final int mRole;
+
+    /**
+     * Accept any form of header compression.
+     *
+     * This option is only meaningful in {@link NetworkRequest}s. Specifiers for actual L2CAP
+     * networks never have this option set.
+     */
+    public static final int HEADER_COMPRESSION_ANY = 0;
+    /** Do not compress packets on this network. */
+    public static final int HEADER_COMPRESSION_NONE = 1;
+    /** Use 6lowpan header compression as specified in rfc6282. */
+    public static final int HEADER_COMPRESSION_6LOWPAN = 2;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(flag = false, prefix = "HEADER_COMPRESSION_", value = {
+        HEADER_COMPRESSION_ANY,
+        HEADER_COMPRESSION_NONE,
+        HEADER_COMPRESSION_6LOWPAN
+    })
+    public @interface HeaderCompression {}
+    /** Header compression mechanism used on this network. */
+    @HeaderCompression
+    private final int mHeaderCompression;
+
+    /** The MAC address of the remote. */
+    @Nullable
+    private final MacAddress mRemoteAddress;
+
+    /**
+     * Match any Protocol/Service Multiplexer (PSM).
+     *
+     * This PSM value is only meaningful in {@link NetworkRequest}s. Specifiers for actual L2CAP
+     * networks never have this value set.
+     */
+    public static final int PSM_ANY = 0;
+
+    /** The Bluetooth L2CAP Protocol/Service Multiplexer (PSM). */
+    private final int mPsm;
+
+    private L2capNetworkSpecifier(Parcel in) {
+        mRole = in.readInt();
+        mHeaderCompression = in.readInt();
+        mRemoteAddress = in.readParcelable(getClass().getClassLoader());
+        mPsm = in.readInt();
+    }
+
+    /** @hide */
+    public L2capNetworkSpecifier(@Role int role, @HeaderCompression int headerCompression,
+            MacAddress remoteAddress, int psm) {
+        mRole = role;
+        mHeaderCompression = headerCompression;
+        mRemoteAddress = remoteAddress;
+        mPsm = psm;
+    }
+
+    /** Returns the role to be used for this network. */
+    @Role
+    public int getRole() {
+        return mRole;
+    }
+
+    /** Returns the compression mechanism for this network. */
+    @HeaderCompression
+    public int getHeaderCompression() {
+        return mHeaderCompression;
+    }
+
+    /**
+     * Returns the remote MAC address for this network to connect to.
+     *
+     * The remote address is only meaningful for networks that have ROLE_CLIENT.
+     *
+     * When receiving this {@link L2capNetworkSpecifier} from Connectivity APIs such as a {@link
+     * ConnectivityManager.NetworkCallback}, the MAC address is redacted.
+     */
+    public @Nullable MacAddress getRemoteAddress() {
+        return mRemoteAddress;
+    }
+
+    /** Returns the Protocol/Service Multiplexer (PSM) for this network to connect to. */
+    public int getPsm() {
+        return mPsm;
+    }
+
+    /**
+     * Checks whether the given L2capNetworkSpecifier is valid as part of a server network
+     * reservation request.
+     *
+     * @hide
+     */
+    public boolean isValidServerReservationSpecifier() {
+        // The ROLE_SERVER offer can be satisfied by a ROLE_ANY request.
+        if (mRole != ROLE_SERVER) return false;
+
+        // HEADER_COMPRESSION_ANY is never valid in a request.
+        if (mHeaderCompression == HEADER_COMPRESSION_ANY) return false;
+
+        // Remote address must be null for ROLE_SERVER requests.
+        if (mRemoteAddress != null) return false;
+
+        // reservation must allocate a PSM, so only PSM_ANY can be passed.
+        if (mPsm != PSM_ANY) return false;
+
+        return true;
+    }
+
+    /**
+     * Checks whether the given L2capNetworkSpecifier is valid as part of a client network request.
+     *
+     * @hide
+     */
+    public boolean isValidClientRequestSpecifier() {
+        // The ROLE_CLIENT offer can be satisfied by a ROLE_ANY request.
+        if (mRole != ROLE_CLIENT) return false;
+
+        // HEADER_COMPRESSION_ANY is never valid in a request.
+        if (mHeaderCompression == HEADER_COMPRESSION_ANY) return false;
+
+        // Remote address must not be null for ROLE_CLIENT requests.
+        if (mRemoteAddress == null) return false;
+
+        // Client network requests require a PSM to be specified.
+        // Ensure the PSM is within the valid range of dynamic BLE L2CAP values.
+        if (mPsm < 0x80) return false;
+        if (mPsm > 0xFF) return false;
+
+        return true;
+    }
+
+    /** A builder class for L2capNetworkSpecifier. */
+    public static final class Builder {
+        @Role
+        private int mRole = ROLE_ANY;
+        @HeaderCompression
+        private int mHeaderCompression = HEADER_COMPRESSION_ANY;
+        @Nullable
+        private MacAddress mRemoteAddress;
+        private int mPsm = PSM_ANY;
+
+        /**
+         * Set the role to use for this network.
+         *
+         * If not set, defaults to {@link ROLE_ANY}.
+         *
+         * @param role the role to use.
+         */
+        @NonNull
+        public Builder setRole(@Role int role) {
+            mRole = role;
+            return this;
+        }
+
+        /**
+         * Set the header compression mechanism to use for this network.
+         *
+         * If not set, defaults to {@link HEADER_COMPRESSION_ANY}. This option must be specified
+         * (i.e. must not be set to {@link HEADER_COMPRESSION_ANY}) when requesting or reserving a
+         * new network.
+         *
+         * @param headerCompression the header compression mechanism to use.
+         */
+        @NonNull
+        public Builder setHeaderCompression(@HeaderCompression int headerCompression) {
+            mHeaderCompression = headerCompression;
+            return this;
+        }
+
+        /**
+         * Set the remote address for the client to connect to.
+         *
+         * Only valid for client networks. If not set, the specifier matches any MAC address.
+         *
+         * @param remoteAddress the MAC address to connect to, or null to match any MAC address.
+         */
+        @NonNull
+        public Builder setRemoteAddress(@Nullable MacAddress remoteAddress) {
+            mRemoteAddress = remoteAddress;
+            return this;
+        }
+
+        /**
+         * Set the Protocol/Service Multiplexer (PSM) for the client to connect to.
+         *
+         * If not set, defaults to {@link PSM_ANY}.
+         *
+         * @param psm the Protocol/Service Multiplexer (PSM) to connect to.
+         */
+        @NonNull
+        public Builder setPsm(@IntRange(from = 0, to = 255) int psm) {
+            if (psm < 0 /* PSM_ANY */ || psm > 0xFF) {
+                throw new IllegalArgumentException("PSM must be PSM_ANY or within range [1, 255]");
+            }
+            mPsm = psm;
+            return this;
+        }
+
+        /** Create the L2capNetworkSpecifier object. */
+        @NonNull
+        public L2capNetworkSpecifier build() {
+            if (mRole == ROLE_SERVER && mRemoteAddress != null) {
+                throw new IllegalArgumentException(
+                        "Specifying a remote address is not valid for server role.");
+            }
+            return new L2capNetworkSpecifier(mRole, mHeaderCompression, mRemoteAddress, mPsm);
+        }
+    }
+
+    /** @hide */
+    @Override
+    public boolean canBeSatisfiedBy(NetworkSpecifier other) {
+        if (!(other instanceof L2capNetworkSpecifier)) return false;
+        final L2capNetworkSpecifier rhs = (L2capNetworkSpecifier) other;
+
+        // A network / offer cannot be ROLE_ANY, but it is added for consistency.
+        if (mRole != rhs.mRole && mRole != ROLE_ANY && rhs.mRole != ROLE_ANY) {
+            return false;
+        }
+
+        if (mHeaderCompression != rhs.mHeaderCompression
+                && mHeaderCompression != HEADER_COMPRESSION_ANY
+                && rhs.mHeaderCompression != HEADER_COMPRESSION_ANY) {
+            return false;
+        }
+
+        if (!Objects.equals(mRemoteAddress, rhs.mRemoteAddress)
+                && mRemoteAddress != null && rhs.mRemoteAddress != null) {
+            return false;
+        }
+
+        if (mPsm != rhs.mPsm && mPsm != PSM_ANY && rhs.mPsm != PSM_ANY) {
+            return false;
+        }
+        return true;
+    }
+
+    /** @hide */
+    @Override
+    @Nullable
+    public NetworkSpecifier redact() {
+        final NetworkSpecifier redactedSpecifier = new Builder()
+                .setRole(mRole)
+                .setHeaderCompression(mHeaderCompression)
+                // The remote address is redacted.
+                .setRemoteAddress(null)
+                .setPsm(mPsm)
+                .build();
+        return redactedSpecifier;
+    }
+
+    /** @hide */
+    @Override
+    public int hashCode() {
+        return Objects.hash(mRole, mHeaderCompression, mRemoteAddress, mPsm);
+    }
+
+    /** @hide */
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+        if (!(obj instanceof L2capNetworkSpecifier)) return false;
+
+        final L2capNetworkSpecifier rhs = (L2capNetworkSpecifier) obj;
+        return mRole == rhs.mRole
+                && mHeaderCompression == rhs.mHeaderCompression
+                && Objects.equals(mRemoteAddress, rhs.mRemoteAddress)
+                && mPsm == rhs.mPsm;
+    }
+
+    /** @hide */
+    @Override
+    public String toString() {
+        final String role;
+        switch (mRole) {
+            case ROLE_CLIENT:
+                role = "ROLE_CLIENT";
+                break;
+            case ROLE_SERVER:
+                role = "ROLE_SERVER";
+                break;
+            default:
+                role = "ROLE_ANY";
+                break;
+        }
+
+        final String headerCompression;
+        switch (mHeaderCompression) {
+            case HEADER_COMPRESSION_NONE:
+                headerCompression = "HEADER_COMPRESSION_NONE";
+                break;
+            case HEADER_COMPRESSION_6LOWPAN:
+                headerCompression = "HEADER_COMPRESSION_6LOWPAN";
+                break;
+            default:
+                headerCompression = "HEADER_COMPRESSION_ANY";
+                break;
+        }
+
+        final String psm = (mPsm == PSM_ANY) ? "PSM_ANY" : String.valueOf(mPsm);
+
+        return String.format("L2capNetworkSpecifier(%s, %s, RemoteAddress=%s, PSM=%s)",
+                role, headerCompression, Objects.toString(mRemoteAddress), psm);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mRole);
+        dest.writeInt(mHeaderCompression);
+        dest.writeParcelable(mRemoteAddress, flags);
+        dest.writeInt(mPsm);
+    }
+
+    public static final @NonNull Creator<L2capNetworkSpecifier> CREATOR = new Creator<>() {
+        @Override
+        public L2capNetworkSpecifier createFromParcel(Parcel in) {
+            return new L2capNetworkSpecifier(in);
+        }
+
+        @Override
+        public L2capNetworkSpecifier[] newArray(int size) {
+            return new L2capNetworkSpecifier[size];
+        }
+    };
+}
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index 4a50397..c6b62ee 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -359,6 +359,7 @@
         mSubIds = new ArraySet<>();
         mUnderlyingNetworks = null;
         mEnterpriseId = 0;
+        mReservationId = RES_ID_UNSET;
     }
 
     /**
@@ -393,6 +394,7 @@
         // necessary.
         mUnderlyingNetworks = nc.mUnderlyingNetworks;
         mEnterpriseId = nc.mEnterpriseId;
+        mReservationId = nc.mReservationId;
     }
 
     /**
@@ -2233,7 +2235,8 @@
                 && (onlyImmutable || satisfiedByUids(nc))
                 && (onlyImmutable || satisfiedBySSID(nc))
                 && (onlyImmutable || satisfiedByRequestor(nc))
-                && (onlyImmutable || satisfiedBySubscriptionIds(nc)));
+                && (onlyImmutable || satisfiedBySubscriptionIds(nc)))
+                && satisfiedByReservationId(nc);
     }
 
     /**
@@ -2347,7 +2350,8 @@
                 && equalsAdministratorUids(that)
                 && equalsSubscriptionIds(that)
                 && equalsUnderlyingNetworks(that)
-                && equalsEnterpriseCapabilitiesId(that);
+                && equalsEnterpriseCapabilitiesId(that)
+                && equalsReservationId(that);
     }
 
     @Override
@@ -2373,7 +2377,9 @@
                 + Arrays.hashCode(mAdministratorUids) * 67
                 + Objects.hashCode(mSubIds) * 71
                 + Objects.hashCode(mUnderlyingNetworks) * 73
-                + mEnterpriseId * 79;
+                + mEnterpriseId * 79
+                + mReservationId * 83;
+
     }
 
     @Override
@@ -2411,6 +2417,7 @@
         dest.writeIntArray(CollectionUtils.toIntArray(mSubIds));
         dest.writeTypedList(mUnderlyingNetworks);
         dest.writeInt(mEnterpriseId & ALL_VALID_ENTERPRISE_IDS);
+        dest.writeInt(mReservationId);
     }
 
     public static final @android.annotation.NonNull Creator<NetworkCapabilities> CREATOR =
@@ -2446,6 +2453,7 @@
                 }
                 netCap.setUnderlyingNetworks(in.createTypedArrayList(Network.CREATOR));
                 netCap.mEnterpriseId = in.readInt() & ALL_VALID_ENTERPRISE_IDS;
+                netCap.mReservationId = in.readInt();
                 return netCap;
             }
             @Override
@@ -2548,6 +2556,11 @@
                     NetworkCapabilities::enterpriseIdNameOf, "&");
         }
 
+        if (mReservationId != RES_ID_UNSET) {
+            final boolean isReservationOffer = (mReservationId == RES_ID_MATCH_ALL_RESERVATIONS);
+            sb.append(" ReservationId: ").append(isReservationOffer ? "*" : mReservationId);
+        }
+
         sb.append(" UnderlyingNetworks: ");
         if (mUnderlyingNetworks != null) {
             sb.append("[");
@@ -2876,6 +2889,65 @@
     }
 
     /**
+     * The reservation ID used by non-reservable Networks and "regular" NetworkOffers.
+     *
+     * Note that {@code NetworkRequest#FIRST_REQUEST_ID} is 1;
+     * @hide
+     */
+    public static final int RES_ID_UNSET = 0;
+
+    /**
+     * The reservation ID used by special NetworkOffers that handle RESERVATION requests.
+     *
+     * NetworkOffers with {@code RES_ID_MATCH_ALL_RESERVATIONS} *only* receive onNetworkNeeded()
+     * callbacks for {@code NetworkRequest.Type.RESERVATION}.
+     * @hide
+     */
+    public static final int RES_ID_MATCH_ALL_RESERVATIONS = -1;
+
+    /**
+     * Unique ID that identifies the network reservation.
+     */
+    private int mReservationId;
+
+    /**
+     * Returns the reservation ID
+     * @hide
+     */
+    public int getReservationId() {
+        return mReservationId;
+    }
+
+    /**
+     * Set the reservation ID
+     * @hide
+     */
+    public void setReservationId(int resId) {
+        mReservationId = resId;
+    }
+
+    private boolean equalsReservationId(@NonNull NetworkCapabilities nc) {
+        return mReservationId == nc.mReservationId;
+    }
+
+    private boolean satisfiedByReservationId(@NonNull NetworkCapabilities nc) {
+        if (mReservationId == RES_ID_UNSET) {
+            // To maintain regular NetworkRequest semantics, a request with a zero reservationId
+            // matches an offer or network with any reservationId except MATCH_ALL_RESERVATIONS.
+            return nc.mReservationId != RES_ID_MATCH_ALL_RESERVATIONS;
+        }
+        // A request with a non-zero reservationId matches only an offer or network with that exact
+        // reservationId (required to match the network that will eventually come up) or
+        // MATCH_ALL_RESERVATIONS (required to match the blanket reservation offer).
+        if (nc.mReservationId == RES_ID_MATCH_ALL_RESERVATIONS) {
+            return true;
+        }
+        return mReservationId == nc.mReservationId;
+    }
+
+
+
+    /**
      * Returns a bitmask of all the applicable redactions (based on the permissions held by the
      * receiving app) to be performed on this object.
      *
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index 89572b3..5a08d44 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -32,6 +32,7 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
+import static android.net.NetworkCapabilities.RES_ID_UNSET;
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
 
 import android.annotation.FlaggedApi;
@@ -193,6 +194,16 @@
      *       callbacks about the single, highest scoring current network
      *       (if any) that matches the specified NetworkCapabilities, or
      *
+     *     - RESERVATION requests behave identically to those of type REQUEST.
+     *       For example, unlike LISTEN, they cause networks to remain
+     *       connected, and they match exactly one network (the best one).
+     *       A RESERVATION generates a unique reservationId in its
+     *       NetworkCapabilities by copying the requestId which affects
+     *       matching. A NetworkProvider can register a "blanket" NetworkOffer
+     *       with reservationId = MATCH_ALL_RESERVATIONS to indicate that it
+     *       is capable of generating NetworkOffers in response to RESERVATION
+     *       requests.
+     *
      *     - TRACK_DEFAULT, which causes the framework to issue callbacks for
      *       the single, highest scoring current network (if any) that will
      *       be chosen for an app, but which cannot cause the framework to
@@ -229,6 +240,7 @@
         BACKGROUND_REQUEST,
         TRACK_SYSTEM_DEFAULT,
         LISTEN_FOR_BEST,
+        RESERVATION,
     };
 
     /**
@@ -245,8 +257,17 @@
         if (nc == null) {
             throw new NullPointerException();
         }
+        if (nc.getReservationId() != RES_ID_UNSET) {
+            throw new IllegalArgumentException("ReservationId must only be set by the system");
+        }
         requestId = rId;
         networkCapabilities = nc;
+        if (type == Type.RESERVATION) {
+            // Conceptually, the reservationId is not related to the requestId; however, the
+            // requestId fulfills the same uniqueness requirements that are needed for the
+            // reservationId, so it can be reused for this purpose.
+            networkCapabilities.setReservationId(rId);
+        }
         this.legacyType = legacyType;
         this.type = type;
     }
@@ -261,6 +282,13 @@
         this.type = that.type;
     }
 
+    private NetworkRequest(Parcel in) {
+        networkCapabilities = NetworkCapabilities.CREATOR.createFromParcel(in);
+        legacyType = in.readInt();
+        requestId = in.readInt();
+        type = Type.valueOf(in.readString());  // IllegalArgumentException if invalid.
+    }
+
     /**
      * Builder used to create {@link NetworkRequest} objects.  Specify the Network features
      * needed in terms of {@link NetworkCapabilities} features
@@ -657,12 +685,7 @@
     public static final @android.annotation.NonNull Creator<NetworkRequest> CREATOR =
         new Creator<NetworkRequest>() {
             public NetworkRequest createFromParcel(Parcel in) {
-                NetworkCapabilities nc = NetworkCapabilities.CREATOR.createFromParcel(in);
-                int legacyType = in.readInt();
-                int requestId = in.readInt();
-                Type type = Type.valueOf(in.readString());  // IllegalArgumentException if invalid.
-                NetworkRequest result = new NetworkRequest(nc, legacyType, requestId, type);
-                return result;
+                return new NetworkRequest(in);
             }
             public NetworkRequest[] newArray(int size) {
                 return new NetworkRequest[size];
@@ -703,7 +726,7 @@
      * @hide
      */
     public boolean isRequest() {
-        return type == Type.REQUEST || type == Type.BACKGROUND_REQUEST;
+        return type == Type.REQUEST || type == Type.BACKGROUND_REQUEST || type == Type.RESERVATION;
     }
 
     /**
diff --git a/framework/src/android/net/TestNetworkManager.java b/framework/src/android/net/TestNetworkManager.java
index 416c6de..cbc7a4f 100644
--- a/framework/src/android/net/TestNetworkManager.java
+++ b/framework/src/android/net/TestNetworkManager.java
@@ -23,8 +23,10 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.List;
 import java.util.Objects;
 
 /**
@@ -196,45 +198,6 @@
     }
 
     /**
-     * Create a tap interface for testing purposes
-     *
-     * @param linkAddrs an array of LinkAddresses to assign to the TAP interface
-     * @return A TestNetworkInterface representing the underlying TAP interface. Close the contained
-     *     ParcelFileDescriptor to tear down the TAP interface.
-     * @hide
-     */
-    @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
-    @NonNull
-    public TestNetworkInterface createTapInterface(@NonNull LinkAddress[] linkAddrs) {
-        try {
-            return mService.createInterface(TAP, CARRIER_UP, BRING_UP, USE_IPV6_PROV_DELAY,
-                    linkAddrs, null /* iface */);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    /**
-     * Create a tap interface for testing purposes
-     *
-     * @param bringUp whether to bring up the interface before returning it.
-     *
-     * @return A ParcelFileDescriptor of the underlying TAP interface. Close this to tear down the
-     *     TAP interface.
-     * @hide
-     */
-    @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
-    @NonNull
-    public TestNetworkInterface createTapInterface(boolean bringUp) {
-        try {
-            return mService.createInterface(TAP, CARRIER_UP, bringUp, USE_IPV6_PROV_DELAY,
-                    NO_ADDRS, null /* iface */);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    /**
      * Create a tap interface with a given interface name for testing purposes
      *
      * @param bringUp whether to bring up the interface before returning it.
@@ -258,26 +221,6 @@
     }
 
     /**
-     * Create a tap interface with or without carrier for testing purposes.
-     *
-     * Note: setting carrierUp = false is not supported until kernel version 6.0.
-     *
-     * @param carrierUp whether the created interface has a carrier or not.
-     * @param bringUp whether to bring up the interface before returning it.
-     * @hide
-     */
-    @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
-    @NonNull
-    public TestNetworkInterface createTapInterface(boolean carrierUp, boolean bringUp) {
-        try {
-            return mService.createInterface(TAP, carrierUp, bringUp, USE_IPV6_PROV_DELAY, NO_ADDRS,
-                    null /* iface */);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    /**
      * Create a tap interface for testing purposes.
      *
      * Note: setting carrierUp = false is not supported until kernel version 6.0.
@@ -300,27 +243,6 @@
     }
 
     /**
-     * Create a tap interface for testing purposes.
-     *
-     * @param disableIpv6ProvisioningDelay whether to disable DAD and RS delay.
-     * @param linkAddrs an array of LinkAddresses to assign to the TAP interface
-     * @return A TestNetworkInterface representing the underlying TAP interface. Close the contained
-     *     ParcelFileDescriptor to tear down the TAP interface.
-     * @hide
-     */
-    @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
-    @NonNull
-    public TestNetworkInterface createTapInterface(boolean disableIpv6ProvisioningDelay,
-            @NonNull LinkAddress[] linkAddrs) {
-        try {
-            return mService.createInterface(TAP, CARRIER_UP, BRING_UP, disableIpv6ProvisioningDelay,
-                    linkAddrs, null /* iface */);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    /**
      * Enable / disable carrier on TestNetworkInterface
      *
      * Note: TUNSETCARRIER is not supported until kernel version 5.0.
@@ -337,4 +259,110 @@
             throw e.rethrowFromSystemServer();
         }
     }
+
+    /**
+     * Represents a request to create a tun/tap interface for testing.
+     *
+     * @hide
+     */
+    public static class TestInterfaceRequest {
+        public final boolean isTun;
+        public final boolean hasCarrier;
+        public final boolean bringUp;
+        public final boolean disableIpv6ProvDelay;
+        @Nullable public final String ifname;
+        public final LinkAddress[] linkAddresses;
+
+        private TestInterfaceRequest(boolean isTun, boolean hasCarrier, boolean bringUp,
+                boolean disableProvDelay, @Nullable String ifname, LinkAddress[] linkAddresses) {
+            this.isTun = isTun;
+            this.hasCarrier = hasCarrier;
+            this.bringUp = bringUp;
+            this.disableIpv6ProvDelay = disableProvDelay;
+            this.ifname = ifname;
+            this.linkAddresses = linkAddresses;
+        }
+
+        /**
+         * Builder class for TestInterfaceRequest
+         *
+         * Defaults to a tap interface with carrier that has been brought up.
+         */
+        public static class Builder {
+            private boolean mIsTun = false;
+            private boolean mHasCarrier = true;
+            private boolean mBringUp = true;
+            private boolean mDisableIpv6ProvDelay = false;
+            @Nullable private String mIfname;
+            private List<LinkAddress> mLinkAddresses = new ArrayList<>();
+
+            /** Create tun interface. */
+            public Builder setTun() {
+                mIsTun = true;
+                return this;
+            }
+
+            /** Create tap interface. */
+            public Builder setTap() {
+                mIsTun = false;
+                return this;
+            }
+
+            /** Configure whether the interface has carrier. */
+            public Builder setHasCarrier(boolean hasCarrier) {
+                mHasCarrier = hasCarrier;
+                return this;
+            }
+
+            /** Configure whether the interface should be brought up. */
+            public Builder setBringUp(boolean bringUp) {
+                mBringUp = bringUp;
+                return this;
+            }
+
+            /** Disable DAD and RS delay. */
+            public Builder setDisableIpv6ProvisioningDelay(boolean disableProvDelay) {
+                mDisableIpv6ProvDelay = disableProvDelay;
+                return this;
+            }
+
+            /** Set the interface name. */
+            public Builder setInterfaceName(@Nullable String ifname) {
+                mIfname = ifname;
+                return this;
+            }
+
+            /** The addresses to configure on the interface. */
+            public Builder addLinkAddress(LinkAddress la) {
+                mLinkAddresses.add(la);
+                return this;
+            }
+
+            /** Build TestInterfaceRequest */
+            public TestInterfaceRequest build() {
+                return new TestInterfaceRequest(mIsTun, mHasCarrier, mBringUp,
+                        mDisableIpv6ProvDelay, mIfname, mLinkAddresses.toArray(new LinkAddress[0]));
+            }
+        }
+    }
+
+    /**
+     * Create a TestNetworkInterface (tun or tap) for testing purposes.
+     *
+     * @param request The request describing the interface to create.
+     * @return A TestNetworkInterface representing the underlying tun/tap interface. Close the
+     *         contained ParcelFileDescriptor to tear down the tun/tap interface.
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
+    @NonNull
+    public TestNetworkInterface createTestInterface(@NonNull TestInterfaceRequest request) {
+        try {
+            // TODO: Make TestInterfaceRequest parcelable and pass it instead.
+            return mService.createInterface(request.isTun, request.hasCarrier, request.bringUp,
+                    request.disableIpv6ProvDelay, request.linkAddresses, request.ifname);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
 }
diff --git a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
index 51df8ab..317854b 100644
--- a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
+++ b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
@@ -135,6 +135,17 @@
     @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public static final long ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE = 74210811L;
 
+    /**
+     * Restrict local network access.
+     *
+     * Apps targeting a release after V will require permissions to access the local network.
+     *
+     * @hide
+     */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT)
+    public static final long RESTRICT_LOCAL_NETWORK = 365139289L;
+
     private ConnectivityCompatChanges() {
     }
 }
diff --git a/nearby/framework/java/android/nearby/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
index e78f999..9d7d144 100644
--- a/nearby/framework/java/android/nearby/NearbyManager.java
+++ b/nearby/framework/java/android/nearby/NearbyManager.java
@@ -46,6 +46,7 @@
 import java.util.WeakHashMap;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
+import java.util.stream.Collectors;
 
 /**
  * This class provides a way to perform Nearby related operations such as scanning, broadcasting
@@ -503,7 +504,7 @@
                     PoweredOffFindingEphemeralId ephemeralId = new PoweredOffFindingEphemeralId();
                     ephemeralId.bytes = eid;
                     return ephemeralId;
-                }).toList();
+                }).collect(Collectors.toUnmodifiableList());
         try {
             mService.setPoweredOffFindingEphemeralIds(ephemeralIdList);
         } catch (RemoteException e) {
diff --git a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
index 66ae79c..ac381b8 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
@@ -89,6 +89,9 @@
                             break;
                         case BroadcastRequest.PRESENCE_VERSION_V1:
                             if (adapter.isLeExtendedAdvertisingSupported()) {
+                                if (mAdvertisingSetCallback == null) {
+                                    mAdvertisingSetCallback = getAdvertisingSetCallback();
+                                }
                                 bluetoothLeAdvertiser.startAdvertisingSet(
                                         getAdvertisingSetParameters(),
                                         advertiseData,
@@ -133,6 +136,11 @@
             }
             mBroadcastListener = null;
             mIsAdvertising = false;
+            // If called startAdvertisingSet() but onAdvertisingSetStopped() is not invoked yet,
+            // using the same mAdvertisingSetCallback will cause new advertising cann't be stopped.
+            // Therefore, release the old mAdvertisingSetCallback and
+            // create a new mAdvertisingSetCallback when calling startAdvertisingSet.
+            mAdvertisingSetCallback = null;
         }
     }
 
diff --git a/nearby/tests/cts/fastpair/AndroidManifest.xml b/nearby/tests/cts/fastpair/AndroidManifest.xml
index 472f4f0..9e1ec70 100644
--- a/nearby/tests/cts/fastpair/AndroidManifest.xml
+++ b/nearby/tests/cts/fastpair/AndroidManifest.xml
@@ -21,6 +21,7 @@
   <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
   <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
   <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
+  <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
 
   <application>
     <uses-library android:name="android.test.runner"/>
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
index 3291223..58d1808 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
@@ -26,7 +26,6 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertThrows;
-import static org.junit.Assume.assumeTrue;
 
 import android.app.UiAutomation;
 import android.bluetooth.test_utils.EnableBluetoothRule;
@@ -79,10 +78,10 @@
 
     @ClassRule public static final EnableBluetoothRule sEnableBluetooth = new EnableBluetoothRule();
 
-    private static final byte[] SALT = new byte[]{1, 2};
-    private static final byte[] SECRET_ID = new byte[]{1, 2, 3, 4};
+    private static final byte[] SALT = new byte[] {1, 2};
+    private static final byte[] SECRET_ID = new byte[] {1, 2, 3, 4};
     private static final byte[] META_DATA_ENCRYPTION_KEY = new byte[14];
-    private static final byte[] AUTHENTICITY_KEY = new byte[]{0, 1, 1, 1};
+    private static final byte[] AUTHENTICITY_KEY = new byte[] {0, 1, 1, 1};
     private static final String DEVICE_NAME = "test_device";
     private static final int BLE_MEDIUM = 1;
 
@@ -91,43 +90,45 @@
     private UiAutomation mUiAutomation =
             InstrumentationRegistry.getInstrumentation().getUiAutomation();
 
-    private ScanRequest mScanRequest = new ScanRequest.Builder()
-            .setScanType(ScanRequest.SCAN_TYPE_FAST_PAIR)
-            .setScanMode(ScanRequest.SCAN_MODE_LOW_LATENCY)
-            .setBleEnabled(true)
-            .build();
+    private ScanRequest mScanRequest =
+            new ScanRequest.Builder()
+                    .setScanType(ScanRequest.SCAN_TYPE_FAST_PAIR)
+                    .setScanMode(ScanRequest.SCAN_MODE_LOW_LATENCY)
+                    .setBleEnabled(true)
+                    .build();
     private PresenceDevice.Builder mBuilder =
             new PresenceDevice.Builder("deviceId", SALT, SECRET_ID, META_DATA_ENCRYPTION_KEY);
 
-    private  ScanCallback mScanCallback = new ScanCallback() {
-        @Override
-        public void onDiscovered(@NonNull NearbyDevice device) {
-        }
+    private ScanCallback mScanCallback =
+            new ScanCallback() {
+                @Override
+                public void onDiscovered(@NonNull NearbyDevice device) {}
 
-        @Override
-        public void onUpdated(@NonNull NearbyDevice device) {
-        }
+                @Override
+                public void onUpdated(@NonNull NearbyDevice device) {}
 
-        @Override
-        public void onLost(@NonNull NearbyDevice device) {
-        }
+                @Override
+                public void onLost(@NonNull NearbyDevice device) {}
 
-        @Override
-        public void onError(int errorCode) {
-        }
-    };
+                @Override
+                public void onError(int errorCode) {}
+            };
 
     private static final Executor EXECUTOR = Executors.newSingleThreadExecutor();
 
     @Before
     public void setUp() {
-        mUiAutomation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG,
-                WRITE_ALLOWLISTED_DEVICE_CONFIG, BLUETOOTH_PRIVILEGED);
-        String nameSpace = SdkLevel.isAtLeastU() ? DeviceConfig.NAMESPACE_NEARBY
-                : DeviceConfig.NAMESPACE_TETHERING;
-        DeviceConfig.setProperty(nameSpace,
-                "nearby_enable_presence_broadcast_legacy",
-                "true", false);
+        mUiAutomation.adoptShellPermissionIdentity(
+                READ_DEVICE_CONFIG,
+                WRITE_DEVICE_CONFIG,
+                WRITE_ALLOWLISTED_DEVICE_CONFIG,
+                BLUETOOTH_PRIVILEGED);
+        String nameSpace =
+                SdkLevel.isAtLeastU()
+                        ? DeviceConfig.NAMESPACE_NEARBY
+                        : DeviceConfig.NAMESPACE_TETHERING;
+        DeviceConfig.setProperty(
+                nameSpace, "nearby_enable_presence_broadcast_legacy", "true", false);
 
         mContext = InstrumentationRegistry.getContext();
         mNearbyManager = mContext.getSystemService(NearbyManager.class);
@@ -144,8 +145,9 @@
     @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void test_startScan_noPrivilegedPermission() {
         mUiAutomation.dropShellPermissionIdentity();
-        assertThrows(SecurityException.class, () -> mNearbyManager
-                .startScan(mScanRequest, EXECUTOR, mScanCallback));
+        assertThrows(
+                SecurityException.class,
+                () -> mNearbyManager.startScan(mScanRequest, EXECUTOR, mScanCallback));
     }
 
     @Test
@@ -159,23 +161,25 @@
     @Test
     @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testStartStopBroadcast() throws InterruptedException {
-        PrivateCredential credential = new PrivateCredential.Builder(SECRET_ID, AUTHENTICITY_KEY,
-                META_DATA_ENCRYPTION_KEY, DEVICE_NAME)
-                .setIdentityType(IDENTITY_TYPE_PRIVATE)
-                .build();
+        PrivateCredential credential =
+                new PrivateCredential.Builder(
+                                SECRET_ID, AUTHENTICITY_KEY, META_DATA_ENCRYPTION_KEY, DEVICE_NAME)
+                        .setIdentityType(IDENTITY_TYPE_PRIVATE)
+                        .build();
         BroadcastRequest broadcastRequest =
                 new PresenceBroadcastRequest.Builder(
-                        Collections.singletonList(BLE_MEDIUM), SALT, credential)
+                                Collections.singletonList(BLE_MEDIUM), SALT, credential)
                         .addAction(123)
                         .build();
 
         CountDownLatch latch = new CountDownLatch(1);
-        BroadcastCallback callback = status -> {
-            latch.countDown();
-            assertThat(status).isEqualTo(BroadcastCallback.STATUS_OK);
-        };
-        mNearbyManager.startBroadcast(broadcastRequest, Executors.newSingleThreadExecutor(),
-                callback);
+        BroadcastCallback callback =
+                status -> {
+                    latch.countDown();
+                    assertThat(status).isEqualTo(BroadcastCallback.STATUS_OK);
+                };
+        mNearbyManager.startBroadcast(
+                broadcastRequest, Executors.newSingleThreadExecutor(), callback);
         latch.await(10, TimeUnit.SECONDS);
         mNearbyManager.stopBroadcast(callback);
     }
@@ -197,9 +201,8 @@
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
     public void testsetPoweredOffFindingEphemeralIds() {
-        // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
-        assumeTrue(SdkLevel.isAtLeastV());
         // Only test supporting devices.
         if (mNearbyManager.getPoweredOffFindingMode()
                 == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
@@ -208,24 +211,22 @@
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
     public void testsetPoweredOffFindingEphemeralIds_noPrivilegedPermission() {
-        // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
-        assumeTrue(SdkLevel.isAtLeastV());
         // Only test supporting devices.
         if (mNearbyManager.getPoweredOffFindingMode()
                 == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
 
         mUiAutomation.dropShellPermissionIdentity();
 
-        assertThrows(SecurityException.class,
+        assertThrows(
+                SecurityException.class,
                 () -> mNearbyManager.setPoweredOffFindingEphemeralIds(List.of(new byte[20])));
     }
 
-
     @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
     public void testSetAndGetPoweredOffFindingMode_enabled() {
-        // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
-        assumeTrue(SdkLevel.isAtLeastV());
         // Only test supporting devices.
         if (mNearbyManager.getPoweredOffFindingMode()
                 == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
@@ -234,30 +235,26 @@
         // enableLocation() has dropped shell permission identity.
         mUiAutomation.adoptShellPermissionIdentity(BLUETOOTH_PRIVILEGED);
 
-        mNearbyManager.setPoweredOffFindingMode(
-                NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED);
+        mNearbyManager.setPoweredOffFindingMode(NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED);
         assertThat(mNearbyManager.getPoweredOffFindingMode())
                 .isEqualTo(NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED);
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
     public void testSetAndGetPoweredOffFindingMode_disabled() {
-        // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
-        assumeTrue(SdkLevel.isAtLeastV());
         // Only test supporting devices.
         if (mNearbyManager.getPoweredOffFindingMode()
                 == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
 
-        mNearbyManager.setPoweredOffFindingMode(
-                NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED);
+        mNearbyManager.setPoweredOffFindingMode(NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED);
         assertThat(mNearbyManager.getPoweredOffFindingMode())
                 .isEqualTo(NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED);
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
     public void testSetPoweredOffFindingMode_noPrivilegedPermission() {
-        // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
-        assumeTrue(SdkLevel.isAtLeastV());
         // Only test supporting devices.
         if (mNearbyManager.getPoweredOffFindingMode()
                 == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
@@ -265,14 +262,16 @@
         enableLocation();
         mUiAutomation.dropShellPermissionIdentity();
 
-        assertThrows(SecurityException.class, () -> mNearbyManager
-                .setPoweredOffFindingMode(NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED));
+        assertThrows(
+                SecurityException.class,
+                () ->
+                        mNearbyManager.setPoweredOffFindingMode(
+                                NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED));
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
     public void testGetPoweredOffFindingMode_noPrivilegedPermission() {
-        // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
-        assumeTrue(SdkLevel.isAtLeastV());
         // Only test supporting devices.
         if (mNearbyManager.getPoweredOffFindingMode()
                 == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java
index 32286e1..a36084b 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java
@@ -18,7 +18,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.junit.Assume.assumeTrue;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyBoolean;
 import static org.mockito.Mockito.anyInt;
@@ -29,12 +28,13 @@
 import android.hardware.bluetooth.finder.Eid;
 import android.hardware.bluetooth.finder.IBluetoothFinder;
 import android.nearby.PoweredOffFindingEphemeralId;
+import android.os.Build;
 import android.os.IBinder;
 import android.os.IBinder.DeathRecipient;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
 
-import com.android.modules.utils.build.SdkLevel;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -44,6 +44,7 @@
 
 import java.util.List;
 
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
 public class BluetoothFinderManagerTest {
     private BluetoothFinderManager mBluetoothFinderManager;
     private boolean mGetServiceCalled = false;
@@ -71,8 +72,6 @@
 
     @Before
     public void setup() {
-        // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
-        assumeTrue(SdkLevel.isAtLeastV());
         MockitoAnnotations.initMocks(this);
         mBluetoothFinderManager = new BluetoothFinderManagerSpy();
     }
@@ -80,16 +79,16 @@
     @Test
     public void testSendEids() throws Exception {
         byte[] eidBytes1 = {
-                (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
-                (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
-                (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
-                (byte) 0xe1, (byte) 0xde
+            (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
+            (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
+            (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
+            (byte) 0xe1, (byte) 0xde
         };
         byte[] eidBytes2 = {
-                (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
-                (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
-                (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
-                (byte) 0xf2, (byte) 0xef
+            (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
+            (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
+            (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
+            (byte) 0xf2, (byte) 0xef
         };
         PoweredOffFindingEphemeralId ephemeralId1 = new PoweredOffFindingEphemeralId();
         PoweredOffFindingEphemeralId ephemeralId2 = new PoweredOffFindingEphemeralId();
@@ -105,8 +104,7 @@
 
     @Test
     public void testSendEids_remoteException() throws Exception {
-        doThrow(new RemoteException())
-                .when(mIBluetoothFinderMock).sendEids(any());
+        doThrow(new RemoteException()).when(mIBluetoothFinderMock).sendEids(any());
         mBluetoothFinderManager.sendEids(List.of());
 
         // Verify that we get the service again following a RemoteException.
@@ -117,8 +115,7 @@
 
     @Test
     public void testSendEids_serviceSpecificException() throws Exception {
-        doThrow(new ServiceSpecificException(1))
-                .when(mIBluetoothFinderMock).sendEids(any());
+        doThrow(new ServiceSpecificException(1)).when(mIBluetoothFinderMock).sendEids(any());
         mBluetoothFinderManager.sendEids(List.of());
     }
 
@@ -134,7 +131,8 @@
     @Test
     public void testSetPoweredOffFinderMode_remoteException() throws Exception {
         doThrow(new RemoteException())
-                .when(mIBluetoothFinderMock).setPoweredOffFinderMode(anyBoolean());
+                .when(mIBluetoothFinderMock)
+                .setPoweredOffFinderMode(anyBoolean());
         mBluetoothFinderManager.setPoweredOffFinderMode(true);
 
         // Verify that we get the service again following a RemoteException.
@@ -146,7 +144,8 @@
     @Test
     public void testSetPoweredOffFinderMode_serviceSpecificException() throws Exception {
         doThrow(new ServiceSpecificException(1))
-                .when(mIBluetoothFinderMock).setPoweredOffFinderMode(anyBoolean());
+                .when(mIBluetoothFinderMock)
+                .setPoweredOffFinderMode(anyBoolean());
         mBluetoothFinderManager.setPoweredOffFinderMode(true);
     }
 
diff --git a/networksecurity/TEST_MAPPING b/networksecurity/TEST_MAPPING
index 20ecbce..448ee84 100644
--- a/networksecurity/TEST_MAPPING
+++ b/networksecurity/TEST_MAPPING
@@ -1,5 +1,17 @@
 {
-  "postsubmit": [
+  "presubmit": [
+    {
+      "name": "CtsNetSecConfigCertificateTransparencyTestCases"
+    },
+    {
+      "name": "CtsNetSecConfigCertificateTransparencyDefaultTestCases"
+    },
+    {
+      "name": "NetSecConfigCertificateTransparencySctLogListTestCases"
+    },
+    {
+      "name": "NetSecConfigCertificateTransparencySctNoLogListTestCases"
+    },
     {
       "name": "NetworkSecurityUnitTests"
     }
diff --git a/networksecurity/service/Android.bp b/networksecurity/service/Android.bp
index a41e6a0..d7aacdb 100644
--- a/networksecurity/service/Android.bp
+++ b/networksecurity/service/Android.bp
@@ -24,16 +24,19 @@
 
     srcs: [
         "src/**/*.java",
+        ":statslog-certificate-transparency-java-gen",
     ],
 
     libs: [
         "framework-configinfrastructure.stubs.module_lib",
         "framework-connectivity-pre-jarjar",
         "service-connectivity-pre-jarjar",
+        "framework-statsd.stubs.module_lib",
     ],
 
     static_libs: [
         "auto_value_annotations",
+        "android.security.flags-aconfig-java-export",
     ],
 
     plugins: [
@@ -48,3 +51,10 @@
     sdk_version: "system_server_current",
     apex_available: ["com.android.tethering"],
 }
+
+genrule {
+    name: "statslog-certificate-transparency-java-gen",
+    tools: ["stats-log-api-gen"],
+    cmd: "$(location stats-log-api-gen) --java $(out) --module certificate_transparency --javaPackage com.android.server.net.ct --javaClass CertificateTransparencyStatsLog",
+    out: ["com/android/server/net/ct/CertificateTransparencyStatsLog.java"],
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
index 56a5ee5..fb42c03 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
@@ -13,30 +13,29 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.server.net.ct;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
+package com.android.server.net.ct;
 
 import android.annotation.RequiresApi;
 import android.app.DownloadManager;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
-import android.content.IntentFilter;
 import android.net.Uri;
 import android.os.Build;
 import android.util.Log;
 
 import androidx.annotation.VisibleForTesting;
 
+import com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState;
 import com.android.server.net.ct.DownloadHelper.DownloadStatus;
 
-import org.json.JSONException;
-import org.json.JSONObject;
-
 import java.io.IOException;
 import java.io.InputStream;
 import java.security.GeneralSecurityException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
 
 /** Helper class to download certificate transparency log files. */
 @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@@ -48,35 +47,33 @@
     private final DataStore mDataStore;
     private final DownloadHelper mDownloadHelper;
     private final SignatureVerifier mSignatureVerifier;
-    private final CertificateTransparencyInstaller mInstaller;
+    private final CertificateTransparencyLogger mLogger;
+
+    private final List<CompatibilityVersion> mCompatVersions = new ArrayList<>();
 
     CertificateTransparencyDownloader(
             Context context,
             DataStore dataStore,
             DownloadHelper downloadHelper,
             SignatureVerifier signatureVerifier,
-            CertificateTransparencyInstaller installer) {
+            CertificateTransparencyLogger logger) {
         mContext = context;
         mSignatureVerifier = signatureVerifier;
         mDataStore = dataStore;
         mDownloadHelper = downloadHelper;
-        mInstaller = installer;
+        mLogger = logger;
     }
 
-    void initialize() {
-        mInstaller.addCompatibilityVersion(Config.COMPATIBILITY_VERSION);
+    void addCompatibilityVersion(CompatibilityVersion compatVersion) {
+        mCompatVersions.add(compatVersion);
+    }
 
-        IntentFilter intentFilter = new IntentFilter();
-        intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
-        mContext.registerReceiver(this, intentFilter, Context.RECEIVER_EXPORTED);
-
-        if (Config.DEBUG) {
-            Log.d(TAG, "CertificateTransparencyDownloader initialized successfully");
-        }
+    void clearCompatibilityVersions() {
+        mCompatVersions.clear();
     }
 
     long startPublicKeyDownload() {
-        long downloadId = download(mDataStore.getProperty(Config.PUBLIC_KEY_URL));
+        long downloadId = download(Config.URL_PUBLIC_KEY);
         if (downloadId != -1) {
             mDataStore.setPropertyLong(Config.PUBLIC_KEY_DOWNLOAD_ID, downloadId);
             mDataStore.store();
@@ -84,19 +81,31 @@
         return downloadId;
     }
 
-    long startMetadataDownload() {
-        long downloadId = download(mDataStore.getProperty(Config.METADATA_URL));
+    private long startMetadataDownload(CompatibilityVersion compatVersion) {
+        long downloadId = download(compatVersion.getMetadataUrl());
         if (downloadId != -1) {
-            mDataStore.setPropertyLong(Config.METADATA_DOWNLOAD_ID, downloadId);
+            mDataStore.setPropertyLong(compatVersion.getMetadataPropertyName(), downloadId);
             mDataStore.store();
         }
         return downloadId;
     }
 
-    long startContentDownload() {
-        long downloadId = download(mDataStore.getProperty(Config.CONTENT_URL));
+    @VisibleForTesting
+    void startMetadataDownload() {
+        for (CompatibilityVersion compatVersion : mCompatVersions) {
+            if (startMetadataDownload(compatVersion) == -1) {
+                Log.e(TAG, "Metadata download not started for " + compatVersion.getCompatVersion());
+            } else if (Config.DEBUG) {
+                Log.d(TAG, "Metadata download started for " + compatVersion.getCompatVersion());
+            }
+        }
+    }
+
+    @VisibleForTesting
+    long startContentDownload(CompatibilityVersion compatVersion) {
+        long downloadId = download(compatVersion.getContentUrl());
         if (downloadId != -1) {
-            mDataStore.setPropertyLong(Config.CONTENT_DOWNLOAD_ID, downloadId);
+            mDataStore.setPropertyLong(compatVersion.getContentPropertyName(), downloadId);
             mDataStore.store();
         }
         return downloadId;
@@ -110,25 +119,28 @@
             return;
         }
 
-        long completedId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
+        long completedId =
+                intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, /* defaultValue= */ -1);
         if (completedId == -1) {
             Log.e(TAG, "Invalid completed download Id");
             return;
         }
 
-        if (isPublicKeyDownloadId(completedId)) {
+        if (getPublicKeyDownloadId() == completedId) {
             handlePublicKeyDownloadCompleted(completedId);
             return;
         }
 
-        if (isMetadataDownloadId(completedId)) {
-            handleMetadataDownloadCompleted(completedId);
-            return;
-        }
+        for (CompatibilityVersion compatVersion : mCompatVersions) {
+            if (getMetadataDownloadId(compatVersion) == completedId) {
+                handleMetadataDownloadCompleted(compatVersion, completedId);
+                return;
+            }
 
-        if (isContentDownloadId(completedId)) {
-            handleContentDownloadCompleted(completedId);
-            return;
+            if (getContentDownloadId(compatVersion) == completedId) {
+                handleContentDownloadCompleted(compatVersion, completedId);
+                return;
+            }
         }
 
         Log.i(TAG, "Download id " + completedId + " is not recognized.");
@@ -154,78 +166,72 @@
             return;
         }
 
-        if (startMetadataDownload() == -1) {
-            Log.e(TAG, "Metadata download not started.");
-        } else if (Config.DEBUG) {
-            Log.d(TAG, "Metadata download started successfully.");
-        }
+        startMetadataDownload();
     }
 
-    private void handleMetadataDownloadCompleted(long downloadId) {
+    private void handleMetadataDownloadCompleted(
+            CompatibilityVersion compatVersion, long downloadId) {
         DownloadStatus status = mDownloadHelper.getDownloadStatus(downloadId);
         if (!status.isSuccessful()) {
             handleDownloadFailed(status);
             return;
         }
-        if (startContentDownload() == -1) {
-            Log.e(TAG, "Content download not started.");
+        if (startContentDownload(compatVersion) == -1) {
+            Log.e(TAG, "Content download failed for" + compatVersion.getCompatVersion());
         } else if (Config.DEBUG) {
-            Log.d(TAG, "Content download started successfully.");
+            Log.d(TAG, "Content download started for" + compatVersion.getCompatVersion());
         }
     }
 
-    private void handleContentDownloadCompleted(long downloadId) {
+    private void handleContentDownloadCompleted(
+            CompatibilityVersion compatVersion, long downloadId) {
         DownloadStatus status = mDownloadHelper.getDownloadStatus(downloadId);
         if (!status.isSuccessful()) {
             handleDownloadFailed(status);
             return;
         }
 
-        Uri contentUri = getContentDownloadUri();
-        Uri metadataUri = getMetadataDownloadUri();
+        Uri contentUri = getContentDownloadUri(compatVersion);
+        Uri metadataUri = getMetadataDownloadUri(compatVersion);
         if (contentUri == null || metadataUri == null) {
             Log.e(TAG, "Invalid URIs");
             return;
         }
 
-        boolean success = false;
-        try {
-            success = mSignatureVerifier.verify(contentUri, metadataUri);
-        } catch (IOException | GeneralSecurityException e) {
-            Log.e(TAG, "Could not verify new log list", e);
-        }
-        if (!success) {
+        LogListUpdateStatus updateStatus = mSignatureVerifier.verify(contentUri, metadataUri);
+
+        if (!updateStatus.isSignatureVerified()) {
             Log.w(TAG, "Log list did not pass verification");
-            return;
-        }
 
-        String version = null;
-        try (InputStream inputStream = mContext.getContentResolver().openInputStream(contentUri)) {
-            version =
-                    new JSONObject(new String(inputStream.readAllBytes(), UTF_8))
-                            .getString("version");
-        } catch (JSONException | IOException e) {
-            Log.e(TAG, "Could not extract version from log list", e);
+            mLogger.logCTLogListUpdateStateChangedEvent(updateStatus);
+
             return;
         }
 
         try (InputStream inputStream = mContext.getContentResolver().openInputStream(contentUri)) {
-            success = mInstaller.install(Config.COMPATIBILITY_VERSION, inputStream, version);
+            updateStatus = compatVersion.install(inputStream, updateStatus.toBuilder());
         } catch (IOException e) {
             Log.e(TAG, "Could not install new content", e);
             return;
         }
 
-        if (success) {
-            // Update information about the stored version on successful install.
-            mDataStore.setProperty(Config.VERSION, version);
-            mDataStore.store();
-        }
+        mLogger.logCTLogListUpdateStateChangedEvent(updateStatus);
     }
 
     private void handleDownloadFailed(DownloadStatus status) {
         Log.e(TAG, "Download failed with " + status);
-        // TODO(378626065): Report failure via statsd.
+
+        LogListUpdateStatus.Builder updateStatusBuilder = LogListUpdateStatus.builder();
+        if (status.isHttpError()) {
+            updateStatusBuilder
+                    .setState(CTLogListUpdateState.HTTP_ERROR)
+                    .setHttpErrorStatusCode(status.reason());
+        } else {
+            // TODO(b/384935059): handle blocked domain logging
+            updateStatusBuilder.setDownloadStatus(Optional.of(status.reason()));
+        }
+
+        mLogger.logCTLogListUpdateStateChangedEvent(updateStatusBuilder.build());
     }
 
     private long download(String url) {
@@ -239,17 +245,19 @@
 
     @VisibleForTesting
     long getPublicKeyDownloadId() {
-        return mDataStore.getPropertyLong(Config.PUBLIC_KEY_DOWNLOAD_ID, -1);
+        return mDataStore.getPropertyLong(Config.PUBLIC_KEY_DOWNLOAD_ID, /* defaultValue= */ -1);
     }
 
     @VisibleForTesting
-    long getMetadataDownloadId() {
-        return mDataStore.getPropertyLong(Config.METADATA_DOWNLOAD_ID, -1);
+    long getMetadataDownloadId(CompatibilityVersion compatVersion) {
+        return mDataStore.getPropertyLong(
+                compatVersion.getMetadataPropertyName(), /* defaultValue */ -1);
     }
 
     @VisibleForTesting
-    long getContentDownloadId() {
-        return mDataStore.getPropertyLong(Config.CONTENT_DOWNLOAD_ID, -1);
+    long getContentDownloadId(CompatibilityVersion compatVersion) {
+        return mDataStore.getPropertyLong(
+                compatVersion.getContentPropertyName(), /* defaultValue= */ -1);
     }
 
     @VisibleForTesting
@@ -259,38 +267,27 @@
 
     @VisibleForTesting
     boolean hasMetadataDownloadId() {
-        return getMetadataDownloadId() != -1;
+        return mCompatVersions.stream()
+                .map(this::getMetadataDownloadId)
+                .anyMatch(downloadId -> downloadId != -1);
     }
 
     @VisibleForTesting
     boolean hasContentDownloadId() {
-        return getContentDownloadId() != -1;
-    }
-
-    @VisibleForTesting
-    boolean isPublicKeyDownloadId(long downloadId) {
-        return getPublicKeyDownloadId() == downloadId;
-    }
-
-    @VisibleForTesting
-    boolean isMetadataDownloadId(long downloadId) {
-        return getMetadataDownloadId() == downloadId;
-    }
-
-    @VisibleForTesting
-    boolean isContentDownloadId(long downloadId) {
-        return getContentDownloadId() == downloadId;
+        return mCompatVersions.stream()
+                .map(this::getContentDownloadId)
+                .anyMatch(downloadId -> downloadId != -1);
     }
 
     private Uri getPublicKeyDownloadUri() {
         return mDownloadHelper.getUri(getPublicKeyDownloadId());
     }
 
-    private Uri getMetadataDownloadUri() {
-        return mDownloadHelper.getUri(getMetadataDownloadId());
+    private Uri getMetadataDownloadUri(CompatibilityVersion compatVersion) {
+        return mDownloadHelper.getUri(getMetadataDownloadId(compatVersion));
     }
 
-    private Uri getContentDownloadUri() {
-        return mDownloadHelper.getUri(getContentDownloadId());
+    private Uri getContentDownloadUri(CompatibilityVersion compatVersion) {
+        return mDownloadHelper.getUri(getContentDownloadId(compatVersion));
     }
 }
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
deleted file mode 100644
index 3138ea7..0000000
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.server.net.ct;
-
-import android.annotation.RequiresApi;
-import android.os.Build;
-import android.provider.DeviceConfig;
-import android.provider.DeviceConfig.Properties;
-import android.text.TextUtils;
-import android.util.Log;
-
-import java.security.GeneralSecurityException;
-import java.util.concurrent.Executors;
-
-/** Listener class for the Certificate Transparency Phenotype flags. */
-@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
-class CertificateTransparencyFlagsListener implements DeviceConfig.OnPropertiesChangedListener {
-
-    private static final String TAG = "CertificateTransparencyFlagsListener";
-
-    private final DataStore mDataStore;
-    private final SignatureVerifier mSignatureVerifier;
-    private final CertificateTransparencyDownloader mCertificateTransparencyDownloader;
-
-    CertificateTransparencyFlagsListener(
-            DataStore dataStore,
-            SignatureVerifier signatureVerifier,
-            CertificateTransparencyDownloader certificateTransparencyDownloader) {
-        mDataStore = dataStore;
-        mSignatureVerifier = signatureVerifier;
-        mCertificateTransparencyDownloader = certificateTransparencyDownloader;
-    }
-
-    void initialize() {
-        mDataStore.load();
-        mCertificateTransparencyDownloader.initialize();
-        DeviceConfig.addOnPropertiesChangedListener(
-                Config.NAMESPACE_NETWORK_SECURITY, Executors.newSingleThreadExecutor(), this);
-        if (Config.DEBUG) {
-            Log.d(TAG, "CertificateTransparencyFlagsListener initialized successfully");
-        }
-        // TODO: handle property changes triggering on boot before registering this listener.
-    }
-
-    @Override
-    public void onPropertiesChanged(Properties properties) {
-        if (!Config.NAMESPACE_NETWORK_SECURITY.equals(properties.getNamespace())) {
-            return;
-        }
-
-        String newPublicKey =
-                DeviceConfig.getString(
-                        Config.NAMESPACE_NETWORK_SECURITY,
-                        Config.FLAG_PUBLIC_KEY,
-                        /* defaultValue= */ "");
-        String newVersion =
-                DeviceConfig.getString(
-                        Config.NAMESPACE_NETWORK_SECURITY,
-                        Config.FLAG_VERSION,
-                        /* defaultValue= */ "");
-        String newContentUrl =
-                DeviceConfig.getString(
-                        Config.NAMESPACE_NETWORK_SECURITY,
-                        Config.FLAG_CONTENT_URL,
-                        /* defaultValue= */ "");
-        String newMetadataUrl =
-                DeviceConfig.getString(
-                        Config.NAMESPACE_NETWORK_SECURITY,
-                        Config.FLAG_METADATA_URL,
-                        /* defaultValue= */ "");
-        if (TextUtils.isEmpty(newPublicKey)
-                || TextUtils.isEmpty(newVersion)
-                || TextUtils.isEmpty(newContentUrl)
-                || TextUtils.isEmpty(newMetadataUrl)) {
-            return;
-        }
-
-        if (Config.DEBUG) {
-            Log.d(TAG, "newPublicKey=" + newPublicKey);
-            Log.d(TAG, "newVersion=" + newVersion);
-            Log.d(TAG, "newContentUrl=" + newContentUrl);
-            Log.d(TAG, "newMetadataUrl=" + newMetadataUrl);
-        }
-
-        String oldVersion = mDataStore.getProperty(Config.VERSION);
-        String oldContentUrl = mDataStore.getProperty(Config.CONTENT_URL);
-        String oldMetadataUrl = mDataStore.getProperty(Config.METADATA_URL);
-
-        if (TextUtils.equals(newVersion, oldVersion)
-                && TextUtils.equals(newContentUrl, oldContentUrl)
-                && TextUtils.equals(newMetadataUrl, oldMetadataUrl)) {
-            Log.i(TAG, "No flag changed, ignoring update");
-            return;
-        }
-
-        try {
-            mSignatureVerifier.setPublicKey(newPublicKey);
-        } catch (GeneralSecurityException | IllegalArgumentException e) {
-            Log.e(TAG, "Error setting the public Key", e);
-            return;
-        }
-
-        // TODO: handle the case where there is already a pending download.
-
-        mDataStore.setProperty(Config.CONTENT_URL, newContentUrl);
-        mDataStore.setProperty(Config.METADATA_URL, newMetadataUrl);
-        mDataStore.store();
-
-        if (mCertificateTransparencyDownloader.startMetadataDownload() == -1) {
-            Log.e(TAG, "Metadata download not started.");
-        } else if (Config.DEBUG) {
-            Log.d(TAG, "Metadata download started successfully.");
-        }
-    }
-}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java
deleted file mode 100644
index 4ca97eb..0000000
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.server.net.ct;
-
-import android.util.Log;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.HashMap;
-import java.util.Map;
-
-/** Installer of CT log lists. */
-public class CertificateTransparencyInstaller {
-
-    private static final String TAG = "CertificateTransparencyInstaller";
-
-    private final Map<String, CompatibilityVersion> mCompatVersions = new HashMap<>();
-
-    // The CT root directory.
-    private final File mRootDirectory;
-
-    public CertificateTransparencyInstaller(File rootDirectory) {
-        mRootDirectory = rootDirectory;
-    }
-
-    public CertificateTransparencyInstaller(String rootDirectoryPath) {
-        this(new File(rootDirectoryPath));
-    }
-
-    public CertificateTransparencyInstaller() {
-        this(Config.CT_ROOT_DIRECTORY_PATH);
-    }
-
-    void addCompatibilityVersion(String versionName) {
-        removeCompatibilityVersion(versionName);
-        CompatibilityVersion newCompatVersion =
-                new CompatibilityVersion(new File(mRootDirectory, versionName));
-        mCompatVersions.put(versionName, newCompatVersion);
-    }
-
-    void removeCompatibilityVersion(String versionName) {
-        CompatibilityVersion compatVersion = mCompatVersions.remove(versionName);
-        if (compatVersion != null && !compatVersion.delete()) {
-            Log.w(TAG, "Could not delete compatibility version directory.");
-        }
-    }
-
-    CompatibilityVersion getCompatibilityVersion(String versionName) {
-        return mCompatVersions.get(versionName);
-    }
-
-    /**
-     * Install a new log list to use during SCT verification.
-     *
-     * @param compatibilityVersion the compatibility version of the new log list
-     * @param newContent an input stream providing the log list
-     * @param version the minor version of the new log list
-     * @return true if the log list was installed successfully, false otherwise.
-     * @throws IOException if the list cannot be saved in the CT directory.
-     */
-    public boolean install(String compatibilityVersion, InputStream newContent, String version)
-            throws IOException {
-        CompatibilityVersion compatVersion = mCompatVersions.get(compatibilityVersion);
-        if (compatVersion == null) {
-            Log.e(TAG, "No compatibility version for " + compatibilityVersion);
-            return false;
-        }
-        // Ensure root directory exists and is readable.
-        DirectoryUtils.makeDir(mRootDirectory);
-
-        if (!compatVersion.install(newContent, version)) {
-            Log.e(TAG, "Failed to install logs for compatibility version " + compatibilityVersion);
-            return false;
-        }
-        Log.i(TAG, "New logs installed at " + compatVersion.getLogsDir());
-        return true;
-    }
-}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
index bf23cb0..e6f1379 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
@@ -17,12 +17,14 @@
 
 import android.annotation.RequiresApi;
 import android.app.AlarmManager;
+import android.app.DownloadManager;
 import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.os.Build;
+import android.os.ConfigUpdate;
 import android.os.SystemClock;
 import android.util.Log;
 
@@ -32,56 +34,88 @@
 
     private static final String TAG = "CertificateTransparencyJob";
 
-    private static final String ACTION_JOB_START = "com.android.server.net.ct.action.JOB_START";
-
     private final Context mContext;
     private final DataStore mDataStore;
     private final CertificateTransparencyDownloader mCertificateTransparencyDownloader;
+    private final CompatibilityVersion mCompatVersion;
     private final AlarmManager mAlarmManager;
+    private final PendingIntent mPendingIntent;
+
+    private boolean mScheduled = false;
+    private boolean mDependenciesReady = false;
 
     /** Creates a new {@link CertificateTransparencyJob} object. */
     public CertificateTransparencyJob(
             Context context,
             DataStore dataStore,
-            CertificateTransparencyDownloader certificateTransparencyDownloader) {
+            CertificateTransparencyDownloader certificateTransparencyDownloader,
+            CompatibilityVersion compatVersion) {
         mContext = context;
         mDataStore = dataStore;
         mCertificateTransparencyDownloader = certificateTransparencyDownloader;
+        mCompatVersion = compatVersion;
+
         mAlarmManager = context.getSystemService(AlarmManager.class);
+        mPendingIntent =
+                PendingIntent.getBroadcast(
+                        mContext,
+                        /* requestCode= */ 0,
+                        new Intent(ConfigUpdate.ACTION_UPDATE_CT_LOGS),
+                        PendingIntent.FLAG_IMMUTABLE);
     }
 
-    void initialize() {
-        mDataStore.load();
-        mCertificateTransparencyDownloader.initialize();
-
-        mContext.registerReceiver(
-                this, new IntentFilter(ACTION_JOB_START), Context.RECEIVER_EXPORTED);
-        mAlarmManager.setInexactRepeating(
-                AlarmManager.ELAPSED_REALTIME,
-                SystemClock.elapsedRealtime(), // schedule first job at earliest convenient time.
-                AlarmManager.INTERVAL_DAY,
-                PendingIntent.getBroadcast(
-                        mContext, 0, new Intent(ACTION_JOB_START), PendingIntent.FLAG_IMMUTABLE));
+    void schedule() {
+        if (!mScheduled) {
+            mContext.registerReceiver(
+                    this,
+                    new IntentFilter(ConfigUpdate.ACTION_UPDATE_CT_LOGS),
+                    Context.RECEIVER_EXPORTED);
+            mAlarmManager.setInexactRepeating(
+                    AlarmManager.ELAPSED_REALTIME,
+                    SystemClock
+                            .elapsedRealtime(), // schedule first job at earliest convenient time.
+                    AlarmManager.INTERVAL_DAY,
+                    mPendingIntent);
+        }
+        mScheduled = true;
 
         if (Config.DEBUG) {
-            Log.d(TAG, "CertificateTransparencyJob scheduled successfully.");
+            Log.d(TAG, "CertificateTransparencyJob scheduled.");
+        }
+    }
+
+    void cancel() {
+        if (mScheduled) {
+            mContext.unregisterReceiver(this);
+            mAlarmManager.cancel(mPendingIntent);
+        }
+        mScheduled = false;
+
+        if (mDependenciesReady) {
+            stopDependencies();
+        }
+        mDependenciesReady = false;
+
+        mCompatVersion.delete();
+
+        if (Config.DEBUG) {
+            Log.d(TAG, "CertificateTransparencyJob canceled.");
         }
     }
 
     @Override
     public void onReceive(Context context, Intent intent) {
-        if (!ACTION_JOB_START.equals(intent.getAction())) {
+        if (!ConfigUpdate.ACTION_UPDATE_CT_LOGS.equals(intent.getAction())) {
             Log.w(TAG, "Received unexpected broadcast with action " + intent);
             return;
         }
         if (Config.DEBUG) {
             Log.d(TAG, "Starting CT daily job.");
         }
-
-        mDataStore.setProperty(Config.CONTENT_URL, Config.URL_LOG_LIST);
-        mDataStore.setProperty(Config.METADATA_URL, Config.URL_SIGNATURE);
-        mDataStore.setProperty(Config.PUBLIC_KEY_URL, Config.URL_PUBLIC_KEY);
-        mDataStore.store();
+        if (!mDependenciesReady) {
+            startDependencies();
+            mDependenciesReady = true;
+        }
 
         if (mCertificateTransparencyDownloader.startPublicKeyDownload() == -1) {
             Log.e(TAG, "Public key download not started.");
@@ -89,4 +123,27 @@
             Log.d(TAG, "Public key download started successfully.");
         }
     }
+
+    private void startDependencies() {
+        mDataStore.load();
+        mCertificateTransparencyDownloader.addCompatibilityVersion(mCompatVersion);
+        mContext.registerReceiver(
+                mCertificateTransparencyDownloader,
+                new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
+                Context.RECEIVER_EXPORTED);
+
+        if (Config.DEBUG) {
+            Log.d(TAG, "CertificateTransparencyJob dependencies ready.");
+        }
+    }
+
+    private void stopDependencies() {
+        mContext.unregisterReceiver(mCertificateTransparencyDownloader);
+        mCertificateTransparencyDownloader.clearCompatibilityVersions();
+        mDataStore.delete();
+
+        if (Config.DEBUG) {
+            Log.d(TAG, "CertificateTransparencyJob dependencies stopped.");
+        }
+    }
 }
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java
new file mode 100644
index 0000000..2a37d8f
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net.ct;
+
+/** Interface with logging to statsd for Certificate Transparency. */
+public interface CertificateTransparencyLogger {
+
+    /**
+     * Logs a CTLogListUpdateStateChanged event to statsd.
+     *
+     * @param updateStatus status object containing details from this update event (e.g. log list
+     * signature, log list timestamp, failure reason if applicable)
+     */
+    void logCTLogListUpdateStateChangedEvent(LogListUpdateStatus updateStatus);
+
+    /**
+     * Intermediate enum for use with CertificateTransparencyStatsLog.
+     *
+     * This enum primarily exists to avoid 100+ char line alert fatigue.
+     */
+    enum CTLogListUpdateState {
+        UNKNOWN_STATE,
+        HTTP_ERROR,
+        LOG_LIST_INVALID,
+        PUBLIC_KEY_NOT_FOUND,
+        SIGNATURE_INVALID,
+        SIGNATURE_NOT_FOUND,
+        SIGNATURE_VERIFICATION_FAILED,
+        SUCCESS,
+        VERSION_ALREADY_EXISTS
+    }
+}
\ No newline at end of file
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java
new file mode 100644
index 0000000..f617523
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net.ct;
+
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_DEVICE_OFFLINE;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_DOWNLOAD_CANNOT_RESUME;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_HTTP_ERROR;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_LOG_LIST_INVALID;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_NO_DISK_SPACE;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_PUBLIC_KEY_NOT_FOUND;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_INVALID;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_NOT_FOUND;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_VERIFICATION;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_TOO_MANY_REDIRECTS;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_UNKNOWN;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_VERSION_ALREADY_EXISTS;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__PENDING_WAITING_FOR_WIFI;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__SUCCESS;
+
+import android.app.DownloadManager;
+
+/** Implementation for logging to statsd for Certificate Transparency. */
+class CertificateTransparencyLoggerImpl implements CertificateTransparencyLogger {
+
+    private final DataStore mDataStore;
+
+    CertificateTransparencyLoggerImpl(DataStore dataStore) {
+        mDataStore = dataStore;
+    }
+
+    @Override
+    public void logCTLogListUpdateStateChangedEvent(LogListUpdateStatus updateStatus) {
+        if (updateStatus.isSuccessful()) {
+            resetFailureCount();
+        } else {
+            updateFailureCount();
+        }
+
+        int updateState =
+                updateStatus
+                        .downloadStatus()
+                        .map(s -> downloadStatusToFailureReason(s))
+                        .orElseGet(() -> localEnumToStatsLogEnum(updateStatus.state()));
+        int failureCount =
+                mDataStore.getPropertyInt(
+                        Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0);
+
+        logCTLogListUpdateStateChangedEvent(
+                updateState,
+                failureCount,
+                updateStatus.httpErrorStatusCode(),
+                updateStatus.signature(),
+                updateStatus.logListTimestamp());
+    }
+
+    private void logCTLogListUpdateStateChangedEvent(
+            int updateState,
+            int failureCount,
+            int httpErrorStatusCode,
+            String signature,
+            long logListTimestamp) {
+        CertificateTransparencyStatsLog.write(
+                CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED,
+                updateState,
+                failureCount,
+                httpErrorStatusCode,
+                signature,
+                logListTimestamp);
+    }
+
+    /**
+     * Resets the number of consecutive log list update failures in the data store back to zero.
+     */
+    private void resetFailureCount() {
+        mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* value= */ 0);
+        mDataStore.store();
+    }
+
+    /**
+     * Updates the data store with the current number of consecutive log list update failures.
+     */
+    private void updateFailureCount() {
+        int failure_count =
+                mDataStore.getPropertyInt(
+                        Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0);
+        int new_failure_count = failure_count + 1;
+
+        mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, new_failure_count);
+        mDataStore.store();
+    }
+
+    /** Converts DownloadStatus reason into failure reason to log. */
+    private int downloadStatusToFailureReason(int downloadStatusReason) {
+        switch (downloadStatusReason) {
+            case DownloadManager.PAUSED_WAITING_TO_RETRY:
+            case DownloadManager.PAUSED_WAITING_FOR_NETWORK:
+                return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_DEVICE_OFFLINE;
+            case DownloadManager.ERROR_UNHANDLED_HTTP_CODE:
+            case DownloadManager.ERROR_HTTP_DATA_ERROR:
+                return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_HTTP_ERROR;
+            case DownloadManager.ERROR_TOO_MANY_REDIRECTS:
+                return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_TOO_MANY_REDIRECTS;
+            case DownloadManager.ERROR_CANNOT_RESUME:
+                return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_DOWNLOAD_CANNOT_RESUME;
+            case DownloadManager.ERROR_INSUFFICIENT_SPACE:
+                return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_NO_DISK_SPACE;
+            case DownloadManager.PAUSED_QUEUED_FOR_WIFI:
+                return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__PENDING_WAITING_FOR_WIFI;
+            default:
+                return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_UNKNOWN;
+        }
+    }
+
+    /** Converts the local enum to the corresponding auto-generated one used by CTStatsLog. */
+    private int localEnumToStatsLogEnum(CTLogListUpdateState updateState) {
+        switch (updateState) {
+            case HTTP_ERROR:
+                return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_HTTP_ERROR;
+            case LOG_LIST_INVALID:
+                return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_LOG_LIST_INVALID;
+            case PUBLIC_KEY_NOT_FOUND:
+                return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_PUBLIC_KEY_NOT_FOUND;
+            case SIGNATURE_INVALID:
+                return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_INVALID;
+            case SIGNATURE_NOT_FOUND:
+                return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_NOT_FOUND;
+            case SIGNATURE_VERIFICATION_FAILED:
+                return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_VERIFICATION;
+            case SUCCESS:
+                return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__SUCCESS;
+            case VERSION_ALREADY_EXISTS:
+                return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_VERSION_ALREADY_EXISTS;
+            case UNKNOWN_STATE:
+            default:
+                return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_UNKNOWN;
+        }
+    }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
index 92b2b09..a71ff7c 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
@@ -13,50 +13,60 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package com.android.server.net.ct;
 
+import static android.security.Flags.certificateTransparencyConfiguration;
+
+import static com.android.net.ct.flags.Flags.certificateTransparencyService;
+
 import android.annotation.RequiresApi;
 import android.content.Context;
 import android.net.ct.ICertificateTransparencyManager;
 import android.os.Build;
 import android.provider.DeviceConfig;
+import android.provider.DeviceConfig.Properties;
+import android.util.Log;
 
-import com.android.net.ct.flags.Flags;
 import com.android.server.SystemService;
 
+import java.util.concurrent.Executors;
+
 /** Implementation of the Certificate Transparency service. */
 @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
-public class CertificateTransparencyService extends ICertificateTransparencyManager.Stub {
+public class CertificateTransparencyService extends ICertificateTransparencyManager.Stub
+        implements DeviceConfig.OnPropertiesChangedListener {
 
-    private final CertificateTransparencyFlagsListener mFlagsListener;
+    private static final String TAG = "CertificateTransparencyService";
+
     private final CertificateTransparencyJob mCertificateTransparencyJob;
 
     /**
      * @return true if the CertificateTransparency service is enabled.
      */
     public static boolean enabled(Context context) {
-        return DeviceConfig.getBoolean(
-                        Config.NAMESPACE_NETWORK_SECURITY, Config.FLAG_SERVICE_ENABLED, false)
-                && Flags.certificateTransparencyService();
+        return certificateTransparencyService() && certificateTransparencyConfiguration();
     }
 
     /** Creates a new {@link CertificateTransparencyService} object. */
     public CertificateTransparencyService(Context context) {
         DataStore dataStore = new DataStore(Config.PREFERENCES_FILE);
-        DownloadHelper downloadHelper = new DownloadHelper(context);
-        SignatureVerifier signatureVerifier = new SignatureVerifier(context);
-        CertificateTransparencyDownloader downloader =
-                new CertificateTransparencyDownloader(
+
+        mCertificateTransparencyJob =
+                new CertificateTransparencyJob(
                         context,
                         dataStore,
-                        downloadHelper,
-                        signatureVerifier,
-                        new CertificateTransparencyInstaller());
-
-        mFlagsListener =
-                new CertificateTransparencyFlagsListener(dataStore, signatureVerifier, downloader);
-        mCertificateTransparencyJob =
-                new CertificateTransparencyJob(context, dataStore, downloader);
+                        new CertificateTransparencyDownloader(
+                                context,
+                                dataStore,
+                                new DownloadHelper(context),
+                                new SignatureVerifier(context),
+                                new CertificateTransparencyLoggerImpl(dataStore)),
+                        new CompatibilityVersion(
+                                Config.COMPATIBILITY_VERSION,
+                                Config.URL_SIGNATURE,
+                                Config.URL_LOG_LIST,
+                                Config.CT_ROOT_DIRECTORY_PATH));
     }
 
     /**
@@ -65,16 +75,46 @@
      * @see com.android.server.SystemService#onBootPhase
      */
     public void onBootPhase(int phase) {
-
         switch (phase) {
             case SystemService.PHASE_BOOT_COMPLETED:
-                if (Flags.certificateTransparencyJob()) {
-                    mCertificateTransparencyJob.initialize();
-                } else {
-                    mFlagsListener.initialize();
-                }
+                DeviceConfig.addOnPropertiesChangedListener(
+                        Config.NAMESPACE_NETWORK_SECURITY,
+                        Executors.newSingleThreadExecutor(),
+                        this);
+                onPropertiesChanged(
+                        new Properties.Builder(Config.NAMESPACE_NETWORK_SECURITY).build());
                 break;
             default:
         }
     }
+
+    @Override
+    public void onPropertiesChanged(Properties properties) {
+        if (!Config.NAMESPACE_NETWORK_SECURITY.equals(properties.getNamespace())) {
+            return;
+        }
+
+        if (DeviceConfig.getBoolean(
+                Config.NAMESPACE_NETWORK_SECURITY,
+                Config.FLAG_SERVICE_ENABLED,
+                /* defaultValue= */ true)) {
+            startService();
+        } else {
+            stopService();
+        }
+    }
+
+    private void startService() {
+        if (Config.DEBUG) {
+            Log.d(TAG, "CertificateTransparencyService start");
+        }
+        mCertificateTransparencyJob.schedule();
+    }
+
+    private void stopService() {
+        if (Config.DEBUG) {
+            Log.d(TAG, "CertificateTransparencyService stop");
+        }
+        mCertificateTransparencyJob.cancel();
+    }
 }
diff --git a/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java b/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
index 27488b5..e8a6e64 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
@@ -15,58 +15,104 @@
  */
 package com.android.server.net.ct;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.annotation.RequiresApi;
+import android.os.Build;
 import android.system.ErrnoException;
 import android.system.Os;
+import android.util.Log;
 
+import com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.file.Files;
 
 /** Represents a compatibility version directory. */
+@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
 class CompatibilityVersion {
 
+    private static final String TAG = "CompatibilityVersion";
+
     static final String LOGS_DIR_PREFIX = "logs-";
     static final String LOGS_LIST_FILE_NAME = "log_list.json";
+    static final String CURRENT_LOGS_DIR_SYMLINK_NAME = "current";
 
-    private static final String CURRENT_LOGS_DIR_SYMLINK_NAME = "current";
+    private final String mCompatVersion;
 
+    private final String mMetadataUrl;
+    private final String mContentUrl;
     private final File mRootDirectory;
+    private final File mVersionDirectory;
     private final File mCurrentLogsDirSymlink;
 
-    private File mCurrentLogsDir = null;
-
-    CompatibilityVersion(File rootDirectory) {
+    CompatibilityVersion(
+            String compatVersion, String metadataUrl, String contentUrl, File rootDirectory) {
+        mCompatVersion = compatVersion;
+        mMetadataUrl = metadataUrl;
+        mContentUrl = contentUrl;
         mRootDirectory = rootDirectory;
-        mCurrentLogsDirSymlink = new File(mRootDirectory, CURRENT_LOGS_DIR_SYMLINK_NAME);
+        mVersionDirectory = new File(rootDirectory, compatVersion);
+        mCurrentLogsDirSymlink = new File(mVersionDirectory, CURRENT_LOGS_DIR_SYMLINK_NAME);
+    }
+
+    CompatibilityVersion(
+            String compatVersion, String metadataUrl, String contentUrl, String rootDirectoryPath) {
+        this(compatVersion, metadataUrl, contentUrl, new File(rootDirectoryPath));
     }
 
     /**
      * Installs a log list within this compatibility version directory.
      *
      * @param newContent an input stream providing the log list
-     * @param version the version number of the log list
+     * @param statusBuilder status obj builder containing details of the log list update process
      * @return true if the log list was installed successfully, false otherwise.
      * @throws IOException if the list cannot be saved in the CT directory.
      */
-    boolean install(InputStream newContent, String version) throws IOException {
-        // To support atomically replacing the old configuration directory with the new there's a
-        // bunch of steps. We create a new directory with the logs and then do an atomic update of
-        // the current symlink to point to the new directory.
-        // 1. Ensure that the root directory exists and is readable.
-        DirectoryUtils.makeDir(mRootDirectory);
+    LogListUpdateStatus install(
+            InputStream newContent, LogListUpdateStatus.Builder statusBuilder) throws IOException {
+        String content = new String(newContent.readAllBytes(), UTF_8);
+        try {
+            JSONObject contentJson = new JSONObject(content);
+            return install(
+                    new ByteArrayInputStream(content.getBytes()),
+                    contentJson.getString("version"),
+                    statusBuilder.setLogListTimestamp(contentJson.getLong("log_list_timestamp")));
+        } catch (JSONException e) {
+            Log.e(TAG, "invalid log list format", e);
 
-        File newLogsDir = new File(mRootDirectory, LOGS_DIR_PREFIX + version);
+            return statusBuilder.setState(CTLogListUpdateState.LOG_LIST_INVALID).build();
+        }
+    }
+
+    LogListUpdateStatus install(
+            InputStream newContent, String version, LogListUpdateStatus.Builder statusBuilder)
+            throws IOException {
+        // To support atomically replacing the old configuration directory with the new
+        // there's a bunch of steps. We create a new directory with the logs and then do
+        // an atomic update of the current symlink to point to the new directory.
+        // 1. Ensure the path to the root and version directories exist and are readable.
+        DirectoryUtils.makeDir(mRootDirectory);
+        DirectoryUtils.makeDir(mVersionDirectory);
+
+        File newLogsDir = new File(mVersionDirectory, LOGS_DIR_PREFIX + version);
         // 2. Handle the corner case where the new directory already exists.
         if (newLogsDir.exists()) {
-            // If the symlink has already been updated then the update died between steps 6 and 7
-            // and so we cannot delete the directory since it is in use.
+            // If the symlink has already been updated then the update died between steps 6
+            // and 7 and so we cannot delete the directory since it is in use.
             if (newLogsDir.getCanonicalPath().equals(mCurrentLogsDirSymlink.getCanonicalPath())) {
+                Log.i(TAG, newLogsDir + " already exists, skipping install.");
                 deleteOldLogDirectories();
-                return false;
+                return statusBuilder.setState(CTLogListUpdateState.VERSION_ALREADY_EXISTS).build();
             }
-            // If the symlink has not been updated then the previous installation failed and this is
-            // a re-attempt. Clean-up leftover files and try again.
+            // If the symlink has not been updated then the previous installation failed and
+            // this is a re-attempt. Clean-up leftover files and try again.
             DirectoryUtils.removeDir(newLogsDir);
         }
         try {
@@ -80,8 +126,8 @@
             }
             DirectoryUtils.setWorldReadable(logListFile);
 
-            // 5. Create temp symlink. We rename this to the target symlink to get an atomic update.
-            File tempSymlink = new File(mRootDirectory, "new_symlink");
+            // 5. Create temp symlink. We rename to the target symlink for an atomic update.
+            File tempSymlink = new File(mVersionDirectory, "new_symlink");
             try {
                 Os.symlink(newLogsDir.getCanonicalPath(), tempSymlink.getCanonicalPath());
             } catch (ErrnoException e) {
@@ -95,17 +141,33 @@
             throw e;
         }
         // 7. Cleanup
-        mCurrentLogsDir = newLogsDir;
+        Log.i(TAG, "New logs installed at " + newLogsDir);
         deleteOldLogDirectories();
-        return true;
+        return statusBuilder.setState(CTLogListUpdateState.SUCCESS).build();
     }
 
-    File getRootDir() {
-        return mRootDirectory;
+    String getCompatVersion() {
+        return mCompatVersion;
     }
 
-    File getLogsDir() {
-        return mCurrentLogsDir;
+    String getMetadataUrl() {
+        return mMetadataUrl;
+    }
+
+    String getMetadataPropertyName() {
+        return mCompatVersion + "_" + Config.METADATA_DOWNLOAD_ID;
+    }
+
+    String getContentUrl() {
+        return mContentUrl;
+    }
+
+    String getContentPropertyName() {
+        return mCompatVersion + "_" + Config.CONTENT_DOWNLOAD_ID;
+    }
+
+    File getVersionDir() {
+        return mVersionDirectory;
     }
 
     File getLogsDirSymlink() {
@@ -113,19 +175,21 @@
     }
 
     File getLogsFile() {
-        return new File(mCurrentLogsDir, LOGS_LIST_FILE_NAME);
+        return new File(mCurrentLogsDirSymlink, LOGS_LIST_FILE_NAME);
     }
 
-    boolean delete() {
-        return DirectoryUtils.removeDir(mRootDirectory);
+    void delete() {
+        if (!DirectoryUtils.removeDir(mVersionDirectory)) {
+            Log.w(TAG, "Could not delete compatibility version directory " + mVersionDirectory);
+        }
     }
 
     private void deleteOldLogDirectories() throws IOException {
-        if (!mRootDirectory.exists()) {
+        if (!mVersionDirectory.exists()) {
             return;
         }
         File currentTarget = mCurrentLogsDirSymlink.getCanonicalFile();
-        for (File file : mRootDirectory.listFiles()) {
+        for (File file : mVersionDirectory.listFiles()) {
             if (!currentTarget.equals(file.getCanonicalFile())
                     && file.getName().startsWith(LOGS_DIR_PREFIX)) {
                 DirectoryUtils.removeDir(file);
diff --git a/networksecurity/service/src/com/android/server/net/ct/Config.java b/networksecurity/service/src/com/android/server/net/ct/Config.java
index 70d8e42..5fdba09 100644
--- a/networksecurity/service/src/com/android/server/net/ct/Config.java
+++ b/networksecurity/service/src/com/android/server/net/ct/Config.java
@@ -48,12 +48,10 @@
 
     // properties
     static final String VERSION = "version";
-    static final String CONTENT_URL = "content_url";
     static final String CONTENT_DOWNLOAD_ID = "content_download_id";
-    static final String METADATA_URL = "metadata_url";
     static final String METADATA_DOWNLOAD_ID = "metadata_download_id";
-    static final String PUBLIC_KEY_URL = "public_key_url";
     static final String PUBLIC_KEY_DOWNLOAD_ID = "public_key_download_id";
+    static final String LOG_LIST_UPDATE_FAILURE_COUNT = "log_list_update_failure_count";
 
     // URLs
     static final String URL_PREFIX = "https://www.gstatic.com/android/certificate_transparency/";
diff --git a/networksecurity/service/src/com/android/server/net/ct/DataStore.java b/networksecurity/service/src/com/android/server/net/ct/DataStore.java
index cd6aebf..1f99efa 100644
--- a/networksecurity/service/src/com/android/server/net/ct/DataStore.java
+++ b/networksecurity/service/src/com/android/server/net/ct/DataStore.java
@@ -44,8 +44,9 @@
         }
         try (InputStream in = new FileInputStream(mPropertyFile)) {
             load(in);
-        } catch (IOException e) {
+        } catch (IOException | IllegalArgumentException e) {
             Log.e(TAG, "Error loading property store", e);
+            delete();
         }
     }
 
@@ -57,6 +58,11 @@
         }
     }
 
+    boolean delete() {
+        clear();
+        return mPropertyFile.delete();
+    }
+
     long getPropertyLong(String key, long defaultValue) {
         return Optional.ofNullable(getProperty(key)).map(Long::parseLong).orElse(defaultValue);
     }
@@ -64,4 +70,12 @@
     Object setPropertyLong(String key, long value) {
         return setProperty(key, Long.toString(value));
     }
+
+    int getPropertyInt(String key, int defaultValue) {
+        return Optional.ofNullable(getProperty(key)).map(Integer::parseInt).orElse(defaultValue);
+    }
+
+    Object setPropertyInt(String key, int value) {
+        return setProperty(key, Integer.toString(value));
+    }
 }
diff --git a/networksecurity/service/src/com/android/server/net/ct/LogListUpdateStatus.java b/networksecurity/service/src/com/android/server/net/ct/LogListUpdateStatus.java
new file mode 100644
index 0000000..3f9b762
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/LogListUpdateStatus.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.net.ct;
+
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.PUBLIC_KEY_NOT_FOUND;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_INVALID;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_NOT_FOUND;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_VERIFICATION_FAILED;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SUCCESS;
+
+import com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.Optional;
+
+/** Class to represent the signature verification status for Certificate Transparency. */
+@AutoValue
+public abstract class LogListUpdateStatus {
+
+    abstract CTLogListUpdateState state();
+
+    abstract String signature();
+
+    abstract long logListTimestamp();
+
+    abstract int httpErrorStatusCode();
+
+    abstract Optional<Integer> downloadStatus();
+
+    boolean isSignatureVerified() {
+        // Check that none of the signature verification failures have been set as the state
+        return state() != PUBLIC_KEY_NOT_FOUND
+                && state() != SIGNATURE_INVALID
+                && state() != SIGNATURE_NOT_FOUND
+                && state() != SIGNATURE_VERIFICATION_FAILED;
+    }
+
+    boolean hasSignature() {
+        return signature() != null && signature().length() > 0;
+    }
+
+    boolean isSuccessful() {
+        return state() == SUCCESS;
+    }
+
+    static LogListUpdateStatus getDefaultInstance() {
+        return builder().build();
+    }
+
+    @AutoValue.Builder
+    abstract static class Builder {
+        abstract Builder setState(CTLogListUpdateState updateState);
+
+        abstract Builder setSignature(String signature);
+
+        abstract Builder setLogListTimestamp(long timestamp);
+
+        abstract Builder setHttpErrorStatusCode(int httpStatusCode);
+
+        abstract Builder setDownloadStatus(Optional<Integer> downloadStatus);
+
+        abstract LogListUpdateStatus build();
+    }
+
+    abstract LogListUpdateStatus.Builder toBuilder();
+
+    static Builder builder() {
+        return new AutoValue_LogListUpdateStatus.Builder()
+            .setState(CTLogListUpdateState.UNKNOWN_STATE)
+            .setSignature("")
+            .setLogListTimestamp(0L)
+            .setHttpErrorStatusCode(0)
+            .setDownloadStatus(Optional.empty());
+    }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java b/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
index 0b775ca..6040ef6 100644
--- a/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
+++ b/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
@@ -15,12 +15,18 @@
  */
 package com.android.server.net.ct;
 
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.PUBLIC_KEY_NOT_FOUND;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_INVALID;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_NOT_FOUND;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_VERIFICATION_FAILED;
+
 import android.annotation.NonNull;
 import android.annotation.RequiresApi;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.net.Uri;
 import android.os.Build;
+import android.util.Log;
 
 import androidx.annotation.VisibleForTesting;
 
@@ -40,6 +46,7 @@
 public class SignatureVerifier {
 
     private final Context mContext;
+    private static final String TAG = "SignatureVerifier";
 
     @NonNull private Optional<PublicKey> mPublicKey = Optional.empty();
 
@@ -63,10 +70,15 @@
     }
 
     void setPublicKey(String publicKey) throws GeneralSecurityException {
+        byte[] decodedPublicKey = null;
+        try {
+            decodedPublicKey = Base64.getDecoder().decode(publicKey);
+        } catch (IllegalArgumentException e) {
+            throw new GeneralSecurityException("Invalid public key base64 encoding", e);
+        }
         setPublicKey(
                 KeyFactory.getInstance("RSA")
-                        .generatePublic(
-                                new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey))));
+                        .generatePublic(new X509EncodedKeySpec(decodedPublicKey)));
     }
 
     @VisibleForTesting
@@ -74,18 +86,53 @@
         mPublicKey = Optional.of(publicKey);
     }
 
-    boolean verify(Uri file, Uri signature) throws GeneralSecurityException, IOException {
+    LogListUpdateStatus verify(Uri file, Uri signature) {
+        LogListUpdateStatus.Builder statusBuilder = LogListUpdateStatus.builder();
+
         if (!mPublicKey.isPresent()) {
-            throw new InvalidKeyException("Missing public key for signature verification");
+            statusBuilder.setState(PUBLIC_KEY_NOT_FOUND);
+            Log.e(TAG, "No public key found for log list verification");
+            return statusBuilder.build();
         }
-        Signature verifier = Signature.getInstance("SHA256withRSA");
-        verifier.initVerify(mPublicKey.get());
+
         ContentResolver contentResolver = mContext.getContentResolver();
 
         try (InputStream fileStream = contentResolver.openInputStream(file);
                 InputStream signatureStream = contentResolver.openInputStream(signature)) {
+            Signature verifier = Signature.getInstance("SHA256withRSA");
+            verifier.initVerify(mPublicKey.get());
             verifier.update(fileStream.readAllBytes());
-            return verifier.verify(signatureStream.readAllBytes());
+
+            byte[] signatureBytes = signatureStream.readAllBytes();
+            statusBuilder.setSignature(new String(signatureBytes));
+            try {
+                byte[] decodedSigBytes = Base64.getDecoder().decode(signatureBytes);
+
+                if (!verifier.verify(decodedSigBytes)) {
+                    // Leave the UpdateState as UNKNOWN_STATE if successful as there are other
+                    // potential failures past the signature verification step
+                    statusBuilder.setState(SIGNATURE_VERIFICATION_FAILED);
+                }
+            } catch (IllegalArgumentException e) {
+                Log.w(TAG, "Invalid signature base64 encoding", e);
+                statusBuilder.setState(SIGNATURE_INVALID);
+                return statusBuilder.build();
+            }
+        } catch (InvalidKeyException e) {
+            Log.e(TAG, "Signature invalid for log list verification", e);
+            statusBuilder.setState(SIGNATURE_INVALID);
+            return statusBuilder.build();
+        } catch (IOException | GeneralSecurityException e) {
+            Log.e(TAG, "Could not verify new log list", e);
+            statusBuilder.setState(SIGNATURE_VERIFICATION_FAILED);
+            return statusBuilder.build();
         }
+
+        // Double check if the signature is empty that we set the state correctly
+        if (!statusBuilder.build().hasSignature()) {
+            statusBuilder.setState(SIGNATURE_NOT_FOUND);
+        }
+
+        return statusBuilder.build();
     }
 }
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
index ffa1283..2af0122 100644
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
@@ -13,19 +13,17 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package com.android.server.net.ct;
 
+import static com.google.common.io.Files.toByteArray;
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
-
 import android.app.DownloadManager;
 import android.app.DownloadManager.Query;
 import android.app.DownloadManager.Request;
@@ -37,6 +35,8 @@
 
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState;
+
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.junit.After;
@@ -44,6 +44,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -60,23 +61,27 @@
 import java.security.PublicKey;
 import java.security.Signature;
 import java.util.Base64;
+import java.util.Optional;
 
 /** Tests for the {@link CertificateTransparencyDownloader}. */
 @RunWith(JUnit4.class)
 public class CertificateTransparencyDownloaderTest {
 
     @Mock private DownloadManager mDownloadManager;
-    @Mock private CertificateTransparencyInstaller mCertificateTransparencyInstaller;
+    @Mock private CertificateTransparencyLogger mLogger;
+    private ArgumentCaptor<LogListUpdateStatus> mUpdateStatusCaptor =
+            ArgumentCaptor.forClass(LogListUpdateStatus.class);
 
     private PrivateKey mPrivateKey;
     private PublicKey mPublicKey;
     private Context mContext;
-    private File mTempFile;
     private DataStore mDataStore;
     private SignatureVerifier mSignatureVerifier;
+    private CompatibilityVersion mCompatVersion;
     private CertificateTransparencyDownloader mCertificateTransparencyDownloader;
 
     private long mNextDownloadId = 666;
+    private static final long LOG_LIST_TIMESTAMP = 123456789L;
 
     @Before
     public void setUp() throws IOException, GeneralSecurityException {
@@ -87,8 +92,7 @@
         mPublicKey = keyPair.getPublic();
 
         mContext = InstrumentationRegistry.getInstrumentation().getContext();
-        mTempFile = File.createTempFile("datastore-test", ".properties");
-        mDataStore = new DataStore(mTempFile);
+        mDataStore = new DataStore(File.createTempFile("datastore-test", ".properties"));
         mSignatureVerifier = new SignatureVerifier(mContext);
         mCertificateTransparencyDownloader =
                 new CertificateTransparencyDownloader(
@@ -96,55 +100,64 @@
                         mDataStore,
                         new DownloadHelper(mDownloadManager),
                         mSignatureVerifier,
-                        mCertificateTransparencyInstaller);
+                        mLogger);
+        mCompatVersion =
+                new CompatibilityVersion(
+                        /* compatVersion= */ "v666",
+                        Config.URL_SIGNATURE,
+                        Config.URL_LOG_LIST,
+                        mContext.getFilesDir());
 
-        prepareDataStore();
         prepareDownloadManager();
+        mDataStore.load();
+        mCertificateTransparencyDownloader.addCompatibilityVersion(mCompatVersion);
     }
 
     @After
     public void tearDown() {
-        mTempFile.delete();
         mSignatureVerifier.resetPublicKey();
+        mCompatVersion.delete();
+        mDataStore.delete();
     }
 
     @Test
     public void testDownloader_startPublicKeyDownload() {
         assertThat(mCertificateTransparencyDownloader.hasPublicKeyDownloadId()).isFalse();
+
         long downloadId = mCertificateTransparencyDownloader.startPublicKeyDownload();
 
         assertThat(mCertificateTransparencyDownloader.hasPublicKeyDownloadId()).isTrue();
-        assertThat(mCertificateTransparencyDownloader.isPublicKeyDownloadId(downloadId)).isTrue();
+        assertThat(mCertificateTransparencyDownloader.getPublicKeyDownloadId())
+                .isEqualTo(downloadId);
     }
 
     @Test
     public void testDownloader_startMetadataDownload() {
         assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
-        long downloadId = mCertificateTransparencyDownloader.startMetadataDownload();
+
+        mCertificateTransparencyDownloader.startMetadataDownload();
 
         assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isTrue();
-        assertThat(mCertificateTransparencyDownloader.isMetadataDownloadId(downloadId)).isTrue();
     }
 
     @Test
     public void testDownloader_startContentDownload() {
         assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
-        long downloadId = mCertificateTransparencyDownloader.startContentDownload();
+
+        mCertificateTransparencyDownloader.startContentDownload(mCompatVersion);
 
         assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isTrue();
-        assertThat(mCertificateTransparencyDownloader.isContentDownloadId(downloadId)).isTrue();
     }
 
     @Test
     public void testDownloader_publicKeyDownloadSuccess_updatePublicKey_startMetadataDownload()
             throws Exception {
-        long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
-        setSuccessfulDownload(publicKeyId, writePublicKeyToFile(mPublicKey));
+        mCertificateTransparencyDownloader.startPublicKeyDownload();
 
         assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
         assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
         mCertificateTransparencyDownloader.onReceive(
-                mContext, makeDownloadCompleteIntent(publicKeyId));
+                mContext, makePublicKeyDownloadCompleteIntent(writePublicKeyToFile(mPublicKey)));
 
         assertThat(mSignatureVerifier.getPublicKey()).hasValue(mPublicKey);
         assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isTrue();
@@ -154,14 +167,14 @@
     public void
             testDownloader_publicKeyDownloadSuccess_updatePublicKeyFail_doNotStartMetadataDownload()
                     throws Exception {
-        long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
-        setSuccessfulDownload(
-                publicKeyId, writeToFile("i_am_not_a_base64_encoded_public_key".getBytes()));
+        mCertificateTransparencyDownloader.startPublicKeyDownload();
 
         assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
         assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
         mCertificateTransparencyDownloader.onReceive(
-                mContext, makeDownloadCompleteIntent(publicKeyId));
+                mContext,
+                makePublicKeyDownloadCompleteIntent(
+                        writeToFile("i_am_not_a_base64_encoded_public_key".getBytes())));
 
         assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
         assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
@@ -169,128 +182,263 @@
 
     @Test
     public void testDownloader_publicKeyDownloadFail_doNotUpdatePublicKey() throws Exception {
-        long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
-        setFailedDownload(
-                publicKeyId, // Failure cases where we give up on the download.
-                DownloadManager.ERROR_INSUFFICIENT_SPACE,
-                DownloadManager.ERROR_HTTP_DATA_ERROR);
-        Intent downloadCompleteIntent = makeDownloadCompleteIntent(publicKeyId);
+        mCertificateTransparencyDownloader.startPublicKeyDownload();
 
         assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
         assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
-        mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
-        mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+        mCertificateTransparencyDownloader.onReceive(
+                mContext,
+                makePublicKeyDownloadFailedIntent(DownloadManager.ERROR_INSUFFICIENT_SPACE));
+        mCertificateTransparencyDownloader.onReceive(
+                mContext, makePublicKeyDownloadFailedIntent(DownloadManager.ERROR_HTTP_DATA_ERROR));
 
         assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
         assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
     }
 
     @Test
+    public void testDownloader_publicKeyDownloadFail_logsFailure()
+            throws Exception {
+        mCertificateTransparencyDownloader.startPublicKeyDownload();
+
+        mCertificateTransparencyDownloader.onReceive(
+                mContext,
+                makePublicKeyDownloadFailedIntent(DownloadManager.ERROR_INSUFFICIENT_SPACE));
+
+        verify(mLogger, times(1))
+                .logCTLogListUpdateStateChangedEvent(
+                        LogListUpdateStatus.builder()
+                                .setDownloadStatus(
+                                        Optional.of(DownloadManager.ERROR_INSUFFICIENT_SPACE))
+                                .build());
+    }
+
+    @Test
     public void testDownloader_metadataDownloadSuccess_startContentDownload() {
-        long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
-        setSuccessfulDownload(metadataId, new File("log_list.sig"));
+        mCertificateTransparencyDownloader.startMetadataDownload();
 
         assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
         mCertificateTransparencyDownloader.onReceive(
-                mContext, makeDownloadCompleteIntent(metadataId));
+                mContext,
+                makeMetadataDownloadCompleteIntent(mCompatVersion, new File("log_list.sig")));
 
         assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isTrue();
     }
 
     @Test
     public void testDownloader_metadataDownloadFail_doNotStartContentDownload() {
-        long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
-        setFailedDownload(
-                metadataId,
-                // Failure cases where we give up on the download.
-                DownloadManager.ERROR_INSUFFICIENT_SPACE,
-                DownloadManager.ERROR_HTTP_DATA_ERROR);
-        Intent downloadCompleteIntent = makeDownloadCompleteIntent(metadataId);
+        mCertificateTransparencyDownloader.startMetadataDownload();
 
         assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
-        mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
-        mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+        mCertificateTransparencyDownloader.onReceive(
+                mContext,
+                makeMetadataDownloadFailedIntent(
+                        mCompatVersion, DownloadManager.ERROR_INSUFFICIENT_SPACE));
+        mCertificateTransparencyDownloader.onReceive(
+                mContext,
+                makeMetadataDownloadFailedIntent(
+                        mCompatVersion, DownloadManager.ERROR_HTTP_DATA_ERROR));
 
         assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
     }
 
     @Test
-    public void testDownloader_contentDownloadSuccess_installSuccess_updateDataStore()
+    public void testDownloader_metadataDownloadFail_logsFailure()
             throws Exception {
+        mCertificateTransparencyDownloader.startMetadataDownload();
+
+        mCertificateTransparencyDownloader.onReceive(
+                mContext,
+                makeMetadataDownloadFailedIntent(
+                        mCompatVersion, DownloadManager.ERROR_INSUFFICIENT_SPACE));
+
+        verify(mLogger, times(1))
+                .logCTLogListUpdateStateChangedEvent(
+                        LogListUpdateStatus.builder()
+                                .setDownloadStatus(
+                                        Optional.of(DownloadManager.ERROR_INSUFFICIENT_SPACE))
+                                .build());
+    }
+
+    @Test
+    public void testDownloader_contentDownloadSuccess_installSuccess() throws Exception {
         String newVersion = "456";
         File logListFile = makeLogListFile(newVersion);
         File metadataFile = sign(logListFile);
         mSignatureVerifier.setPublicKey(mPublicKey);
-        long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
-        setSuccessfulDownload(metadataId, metadataFile);
-        long contentId = mCertificateTransparencyDownloader.startContentDownload();
-        setSuccessfulDownload(contentId, logListFile);
-        when(mCertificateTransparencyInstaller.install(
-                        eq(Config.COMPATIBILITY_VERSION), any(), anyString()))
-                .thenReturn(true);
+        mCertificateTransparencyDownloader.startMetadataDownload();
 
         assertNoVersionIsInstalled();
         mCertificateTransparencyDownloader.onReceive(
-                mContext, makeDownloadCompleteIntent(contentId));
+                mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+        mCertificateTransparencyDownloader.onReceive(
+                mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
 
         assertInstallSuccessful(newVersion);
     }
 
     @Test
     public void testDownloader_contentDownloadFail_doNotInstall() throws Exception {
-        long contentId = mCertificateTransparencyDownloader.startContentDownload();
-        setFailedDownload(
-                contentId,
-                // Failure cases where we give up on the download.
-                DownloadManager.ERROR_INSUFFICIENT_SPACE,
-                DownloadManager.ERROR_HTTP_DATA_ERROR);
-        Intent downloadCompleteIntent = makeDownloadCompleteIntent(contentId);
+        mCertificateTransparencyDownloader.startContentDownload(mCompatVersion);
 
-        mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
-        mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+        mCertificateTransparencyDownloader.onReceive(
+                mContext,
+                makeContentDownloadFailedIntent(
+                        mCompatVersion, DownloadManager.ERROR_INSUFFICIENT_SPACE));
+        mCertificateTransparencyDownloader.onReceive(
+                mContext,
+                makeContentDownloadFailedIntent(
+                        mCompatVersion, DownloadManager.ERROR_HTTP_DATA_ERROR));
 
-        verify(mCertificateTransparencyInstaller, never()).install(any(), any(), any());
         assertNoVersionIsInstalled();
     }
 
     @Test
-    public void testDownloader_contentDownloadSuccess_installFail_doNotUpdateDataStore()
+    public void testDownloader_contentDownloadFail_logsFailure()
             throws Exception {
-        File logListFile = makeLogListFile("456");
-        File metadataFile = sign(logListFile);
+        mCertificateTransparencyDownloader.startContentDownload(mCompatVersion);
+
+        mCertificateTransparencyDownloader.onReceive(
+                mContext,
+                makeContentDownloadFailedIntent(
+                        mCompatVersion, DownloadManager.ERROR_INSUFFICIENT_SPACE));
+
+        verify(mLogger, times(1))
+                .logCTLogListUpdateStateChangedEvent(
+                        LogListUpdateStatus.builder()
+                                .setDownloadStatus(
+                                        Optional.of(DownloadManager.ERROR_INSUFFICIENT_SPACE))
+                                .build());
+    }
+
+    @Test
+    public void testDownloader_contentDownloadSuccess_invalidLogList_installFails()
+            throws Exception {
+        File invalidLogListFile = writeToFile("not_a_json_log_list".getBytes());
+        File metadataFile = sign(invalidLogListFile);
         mSignatureVerifier.setPublicKey(mPublicKey);
-        long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
-        setSuccessfulDownload(metadataId, metadataFile);
-        long contentId = mCertificateTransparencyDownloader.startContentDownload();
-        setSuccessfulDownload(contentId, logListFile);
-        when(mCertificateTransparencyInstaller.install(
-                        eq(Config.COMPATIBILITY_VERSION), any(), anyString()))
-                .thenReturn(false);
+        mCertificateTransparencyDownloader.startMetadataDownload();
 
         assertNoVersionIsInstalled();
         mCertificateTransparencyDownloader.onReceive(
-                mContext, makeDownloadCompleteIntent(contentId));
+                mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+        mCertificateTransparencyDownloader.onReceive(
+                mContext, makeContentDownloadCompleteIntent(mCompatVersion, invalidLogListFile));
 
         assertNoVersionIsInstalled();
     }
 
     @Test
+    public void
+            testDownloader_contentDownloadSuccess_noPublicKeyFound_logsSingleFailure()
+                    throws Exception {
+        File logListFile = makeLogListFile("456");
+        File metadataFile = sign(logListFile);
+        mSignatureVerifier.setPublicKey(mPublicKey);
+        mCertificateTransparencyDownloader.startMetadataDownload();
+
+        // Set the public key to be missing
+        mSignatureVerifier.resetPublicKey();
+        mCertificateTransparencyDownloader.onReceive(
+                mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+        mCertificateTransparencyDownloader.onReceive(
+                mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
+
+        verify(mLogger, times(1))
+                .logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
+        assertThat(mUpdateStatusCaptor.getValue().state())
+                .isEqualTo(CTLogListUpdateState.PUBLIC_KEY_NOT_FOUND);
+    }
+
+    @Test
+    public void
+            testDownloader_contentDownloadSuccess_wrongSignatureAlgo_logsSingleFailure()
+                    throws Exception {
+        // Arrange
+        File logListFile = makeLogListFile("456");
+        File metadataFile = sign(logListFile);
+
+        // Set the key to be deliberately wrong by using diff algorithm
+        KeyPairGenerator instance = KeyPairGenerator.getInstance("EC");
+        mSignatureVerifier.setPublicKey(instance.generateKeyPair().getPublic());
+
+        // Act
+        mCertificateTransparencyDownloader.startMetadataDownload();
+        mCertificateTransparencyDownloader.onReceive(
+                mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+        mCertificateTransparencyDownloader.onReceive(
+                mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
+
+        // Assert
+        verify(mLogger, times(1))
+                .logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
+        assertThat(mUpdateStatusCaptor.getValue().state())
+                .isEqualTo(CTLogListUpdateState.SIGNATURE_INVALID);
+    }
+
+    @Test
+    public void
+            testDownloader_contentDownloadSuccess_signatureNotVerified_logsSingleFailure()
+                    throws Exception {
+        // Arrange
+        File logListFile = makeLogListFile("456");
+        File metadataFile = sign(logListFile);
+
+        // Set the key to be deliberately wrong by using diff key pair
+        KeyPairGenerator instance = KeyPairGenerator.getInstance("RSA");
+        mSignatureVerifier.setPublicKey(instance.generateKeyPair().getPublic());
+
+        // Act
+        mCertificateTransparencyDownloader.startMetadataDownload();
+        mCertificateTransparencyDownloader.onReceive(
+                mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+        mCertificateTransparencyDownloader.onReceive(
+                mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
+
+        // Assert
+        verify(mLogger, times(1))
+                .logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
+        LogListUpdateStatus statusValue = mUpdateStatusCaptor.getValue();
+        assertThat(statusValue.state())
+                .isEqualTo(CTLogListUpdateState.SIGNATURE_VERIFICATION_FAILED);
+        assertThat(statusValue.signature()).isEqualTo(new String(toByteArray(metadataFile)));
+    }
+
+    @Test
+    public void
+            testDownloader_contentDownloadSuccess_installFail_logsFailure()
+                    throws Exception {
+        File invalidLogListFile = writeToFile("not_a_json_log_list".getBytes());
+        File metadataFile = sign(invalidLogListFile);
+        mSignatureVerifier.setPublicKey(mPublicKey);
+
+        mCertificateTransparencyDownloader.startMetadataDownload();
+        mCertificateTransparencyDownloader.onReceive(
+                mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+        mCertificateTransparencyDownloader.onReceive(
+                mContext, makeContentDownloadCompleteIntent(mCompatVersion, invalidLogListFile));
+
+        verify(mLogger, times(1))
+                .logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
+        LogListUpdateStatus statusValue = mUpdateStatusCaptor.getValue();
+        assertThat(statusValue.state()).isEqualTo(CTLogListUpdateState.LOG_LIST_INVALID);
+        assertThat(statusValue.signature()).isEqualTo(new String(toByteArray(metadataFile)));
+    }
+
+    @Test
     public void testDownloader_contentDownloadSuccess_verificationFail_doNotInstall()
             throws Exception {
         File logListFile = makeLogListFile("456");
         File metadataFile = File.createTempFile("log_list-wrong_metadata", "sig");
         mSignatureVerifier.setPublicKey(mPublicKey);
-        long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
-        setSuccessfulDownload(metadataId, metadataFile);
-        long contentId = mCertificateTransparencyDownloader.startContentDownload();
-        setSuccessfulDownload(contentId, logListFile);
 
         assertNoVersionIsInstalled();
+        mCertificateTransparencyDownloader.startMetadataDownload();
         mCertificateTransparencyDownloader.onReceive(
-                mContext, makeDownloadCompleteIntent(contentId));
+                mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+        mCertificateTransparencyDownloader.onReceive(
+                mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
 
-        verify(mCertificateTransparencyInstaller, never())
-                .install(eq(Config.COMPATIBILITY_VERSION), any(), anyString());
         assertNoVersionIsInstalled();
     }
 
@@ -300,22 +448,20 @@
         File logListFile = makeLogListFile("456");
         File metadataFile = sign(logListFile);
         mSignatureVerifier.resetPublicKey();
-        long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
-        setSuccessfulDownload(metadataId, metadataFile);
-        long contentId = mCertificateTransparencyDownloader.startContentDownload();
-        setSuccessfulDownload(contentId, logListFile);
 
         assertNoVersionIsInstalled();
+        mCertificateTransparencyDownloader.startMetadataDownload();
         mCertificateTransparencyDownloader.onReceive(
-                mContext, makeDownloadCompleteIntent(contentId));
+                mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+        mCertificateTransparencyDownloader.onReceive(
+                mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
 
-        verify(mCertificateTransparencyInstaller, never())
-                .install(eq(Config.COMPATIBILITY_VERSION), any(), anyString());
         assertNoVersionIsInstalled();
     }
 
     @Test
-    public void testDownloader_endToEndSuccess_installNewVersion() throws Exception {
+    public void testDownloader_endToEndSuccess_installNewVersion_andLogsSuccess() throws Exception {
+        // Arrange
         String newVersion = "456";
         File logListFile = makeLogListFile(newVersion);
         File metadataFile = sign(logListFile);
@@ -323,53 +469,47 @@
 
         assertNoVersionIsInstalled();
 
+        // Act
         // 1. Start download of public key.
-        long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
+        mCertificateTransparencyDownloader.startPublicKeyDownload();
 
-        // 2. On successful public key download, set the key and start the metatadata download.
-        setSuccessfulDownload(publicKeyId, publicKeyFile);
-
+        // 2. On successful public key download, set the key and start the metatadata
+        // download.
         mCertificateTransparencyDownloader.onReceive(
-                mContext, makeDownloadCompleteIntent(publicKeyId));
+                mContext, makePublicKeyDownloadCompleteIntent(publicKeyFile));
 
         // 3. On successful metadata download, start the content download.
-        long metadataId = mCertificateTransparencyDownloader.getMetadataDownloadId();
-        setSuccessfulDownload(metadataId, metadataFile);
-
         mCertificateTransparencyDownloader.onReceive(
-                mContext, makeDownloadCompleteIntent(metadataId));
+                mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
 
-        // 4. On successful content download, verify the signature and install the new version.
-        long contentId = mCertificateTransparencyDownloader.getContentDownloadId();
-        setSuccessfulDownload(contentId, logListFile);
-        when(mCertificateTransparencyInstaller.install(
-                        eq(Config.COMPATIBILITY_VERSION), any(), anyString()))
-                .thenReturn(true);
-
+        // 4. On successful content download, verify the signature and install the new
+        // version.
         mCertificateTransparencyDownloader.onReceive(
-                mContext, makeDownloadCompleteIntent(contentId));
+                mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
 
+        // Assert
         assertInstallSuccessful(newVersion);
+        verify(mLogger, times(1))
+                .logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
+
+        LogListUpdateStatus statusValue = mUpdateStatusCaptor.getValue();
+        assertThat(statusValue.state()).isEqualTo(CTLogListUpdateState.SUCCESS);
+        assertThat(statusValue.signature()).isEqualTo(new String(toByteArray(metadataFile)));
+        assertThat(statusValue.logListTimestamp()).isEqualTo(LOG_LIST_TIMESTAMP);
     }
 
     private void assertNoVersionIsInstalled() {
-        assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
+        assertThat(mCompatVersion.getVersionDir().exists()).isFalse();
     }
 
     private void assertInstallSuccessful(String version) {
-        assertThat(mDataStore.getProperty(Config.VERSION)).isEqualTo(version);
-    }
-
-    private Intent makeDownloadCompleteIntent(long downloadId) {
-        return new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
-                .putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId);
-    }
-
-    private void prepareDataStore() {
-        mDataStore.load();
-        mDataStore.setProperty(Config.CONTENT_URL, Config.URL_LOG_LIST);
-        mDataStore.setProperty(Config.METADATA_URL, Config.URL_SIGNATURE);
-        mDataStore.setProperty(Config.PUBLIC_KEY_URL, Config.URL_PUBLIC_KEY);
+        File logsDir =
+                new File(
+                        mCompatVersion.getVersionDir(),
+                        CompatibilityVersion.LOGS_DIR_PREFIX + version);
+        assertThat(logsDir.exists()).isTrue();
+        File logsFile = new File(logsDir, CompatibilityVersion.LOGS_LIST_FILE_NAME);
+        assertThat(logsFile.exists()).isTrue();
     }
 
     private void prepareDownloadManager() {
@@ -377,6 +517,32 @@
                 .thenAnswer(invocation -> mNextDownloadId++);
     }
 
+    private Intent makePublicKeyDownloadCompleteIntent(File publicKeyfile) {
+        return makeDownloadCompleteIntent(
+                mCertificateTransparencyDownloader.getPublicKeyDownloadId(), publicKeyfile);
+    }
+
+    private Intent makeMetadataDownloadCompleteIntent(
+            CompatibilityVersion compatVersion, File signatureFile) {
+        return makeDownloadCompleteIntent(
+                mCertificateTransparencyDownloader.getMetadataDownloadId(compatVersion),
+                signatureFile);
+    }
+
+    private Intent makeContentDownloadCompleteIntent(
+            CompatibilityVersion compatVersion, File logListFile) {
+        return makeDownloadCompleteIntent(
+                mCertificateTransparencyDownloader.getContentDownloadId(compatVersion),
+                logListFile);
+    }
+
+    private Intent makeDownloadCompleteIntent(long downloadId, File file) {
+        when(mDownloadManager.query(any(Query.class))).thenReturn(makeSuccessfulDownloadCursor());
+        when(mDownloadManager.getUriForDownloadedFile(downloadId)).thenReturn(Uri.fromFile(file));
+        return new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
+                .putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId);
+    }
+
     private Cursor makeSuccessfulDownloadCursor() {
         MatrixCursor cursor =
                 new MatrixCursor(
@@ -387,9 +553,26 @@
         return cursor;
     }
 
-    private void setSuccessfulDownload(long downloadId, File file) {
-        when(mDownloadManager.query(any(Query.class))).thenReturn(makeSuccessfulDownloadCursor());
-        when(mDownloadManager.getUriForDownloadedFile(downloadId)).thenReturn(Uri.fromFile(file));
+    private Intent makePublicKeyDownloadFailedIntent(int error) {
+        return makeDownloadFailedIntent(
+                mCertificateTransparencyDownloader.getPublicKeyDownloadId(), error);
+    }
+
+    private Intent makeMetadataDownloadFailedIntent(CompatibilityVersion compatVersion, int error) {
+        return makeDownloadFailedIntent(
+                mCertificateTransparencyDownloader.getMetadataDownloadId(compatVersion), error);
+    }
+
+    private Intent makeContentDownloadFailedIntent(CompatibilityVersion compatVersion, int error) {
+        return makeDownloadFailedIntent(
+                mCertificateTransparencyDownloader.getContentDownloadId(compatVersion), error);
+    }
+
+    private Intent makeDownloadFailedIntent(long downloadId, int error) {
+        when(mDownloadManager.query(any(Query.class))).thenReturn(makeFailedDownloadCursor(error));
+        when(mDownloadManager.getUriForDownloadedFile(downloadId)).thenReturn(null);
+        return new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
+                .putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId);
     }
 
     private Cursor makeFailedDownloadCursor(int error) {
@@ -402,16 +585,6 @@
         return cursor;
     }
 
-    private void setFailedDownload(long downloadId, int... downloadManagerErrors) {
-        Cursor first = makeFailedDownloadCursor(downloadManagerErrors[0]);
-        Cursor[] others = new Cursor[downloadManagerErrors.length - 1];
-        for (int i = 1; i < downloadManagerErrors.length; i++) {
-            others[i - 1] = makeFailedDownloadCursor(downloadManagerErrors[i]);
-        }
-        when(mDownloadManager.query(any())).thenReturn(first, others);
-        when(mDownloadManager.getUriForDownloadedFile(downloadId)).thenReturn(null);
-    }
-
     private File writePublicKeyToFile(PublicKey publicKey)
             throws IOException, GeneralSecurityException {
         return writeToFile(Base64.getEncoder().encode(publicKey.getEncoded()));
@@ -431,7 +604,11 @@
         File logListFile = File.createTempFile("log_list", "json");
 
         try (OutputStream outputStream = new FileOutputStream(logListFile)) {
-            outputStream.write(new JSONObject().put("version", version).toString().getBytes(UTF_8));
+            JSONObject contentJson =
+                    new JSONObject()
+                            .put("version", version)
+                            .put("log_list_timestamp", LOG_LIST_TIMESTAMP);
+            outputStream.write(contentJson.toString().getBytes());
         }
 
         return logListFile;
@@ -445,7 +622,7 @@
         try (InputStream fileStream = new FileInputStream(file);
                 OutputStream outputStream = new FileOutputStream(signatureFile)) {
             signer.update(fileStream.readAllBytes());
-            outputStream.write(signer.sign());
+            outputStream.write(Base64.getEncoder().encode(signer.sign()));
         }
 
         return signatureFile;
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java
deleted file mode 100644
index 50d3f23..0000000
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.server.net.ct;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-/** Tests for the {@link CertificateTransparencyInstaller}. */
-@RunWith(JUnit4.class)
-public class CertificateTransparencyInstallerTest {
-
-    private static final String TEST_VERSION = "test-v1";
-
-    private File mTestDir =
-            new File(
-                    InstrumentationRegistry.getInstrumentation().getContext().getFilesDir(),
-                    "test-dir");
-    private CertificateTransparencyInstaller mCertificateTransparencyInstaller =
-            new CertificateTransparencyInstaller(mTestDir);
-
-    @Before
-    public void setUp() {
-        mCertificateTransparencyInstaller.addCompatibilityVersion(TEST_VERSION);
-    }
-
-    @After
-    public void tearDown() {
-        mCertificateTransparencyInstaller.removeCompatibilityVersion(TEST_VERSION);
-        DirectoryUtils.removeDir(mTestDir);
-    }
-
-    @Test
-    public void testCompatibilityVersion_installSuccessful() throws IOException {
-        assertThat(mTestDir.mkdir()).isTrue();
-        String content = "i_am_compatible";
-        String version = "i_am_version";
-        CompatibilityVersion compatVersion =
-                mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION);
-
-        try (InputStream inputStream = asStream(content)) {
-            assertThat(compatVersion.install(inputStream, version)).isTrue();
-        }
-        File logsDir = compatVersion.getLogsDir();
-        assertThat(logsDir.exists()).isTrue();
-        assertThat(logsDir.isDirectory()).isTrue();
-        assertThat(logsDir.getAbsolutePath())
-                .startsWith(mTestDir.getAbsolutePath() + "/" + TEST_VERSION);
-        File logsListFile = compatVersion.getLogsFile();
-        assertThat(logsListFile.exists()).isTrue();
-        assertThat(logsListFile.getAbsolutePath()).startsWith(logsDir.getAbsolutePath());
-        assertThat(readAsString(logsListFile)).isEqualTo(content);
-        File logsSymlink = compatVersion.getLogsDirSymlink();
-        assertThat(logsSymlink.exists()).isTrue();
-        assertThat(logsSymlink.isDirectory()).isTrue();
-        assertThat(logsSymlink.getAbsolutePath())
-                .startsWith(mTestDir.getAbsolutePath() + "/" + TEST_VERSION + "/current");
-        assertThat(logsSymlink.getCanonicalPath()).isEqualTo(logsDir.getCanonicalPath());
-
-        assertThat(compatVersion.delete()).isTrue();
-        assertThat(logsDir.exists()).isFalse();
-        assertThat(logsSymlink.exists()).isFalse();
-        assertThat(logsListFile.exists()).isFalse();
-    }
-
-    @Test
-    public void testCompatibilityVersion_versionInstalledFailed() throws IOException {
-        assertThat(mTestDir.mkdir()).isTrue();
-
-        CompatibilityVersion compatVersion =
-                mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION);
-        File rootDir = compatVersion.getRootDir();
-        assertThat(rootDir.mkdir()).isTrue();
-
-        String existingVersion = "666";
-        File existingLogDir =
-                new File(rootDir, CompatibilityVersion.LOGS_DIR_PREFIX + existingVersion);
-        assertThat(existingLogDir.mkdir()).isTrue();
-
-        String existingContent = "somebody_tried_to_install_me_but_failed_halfway_through";
-        File logsListFile = new File(existingLogDir, CompatibilityVersion.LOGS_LIST_FILE_NAME);
-        assertThat(logsListFile.createNewFile()).isTrue();
-        writeToFile(logsListFile, existingContent);
-
-        String newContent = "i_am_the_real_content";
-        try (InputStream inputStream = asStream(newContent)) {
-            assertThat(compatVersion.install(inputStream, existingVersion)).isTrue();
-        }
-
-        assertThat(readAsString(logsListFile)).isEqualTo(newContent);
-    }
-
-    @Test
-    public void testCertificateTransparencyInstaller_installSuccessfully() throws IOException {
-        String content = "i_am_a_certificate_and_i_am_transparent";
-        String version = "666";
-
-        try (InputStream inputStream = asStream(content)) {
-            assertThat(
-                            mCertificateTransparencyInstaller.install(
-                                    TEST_VERSION, inputStream, version))
-                    .isTrue();
-        }
-
-        assertThat(mTestDir.exists()).isTrue();
-        assertThat(mTestDir.isDirectory()).isTrue();
-        CompatibilityVersion compatVersion =
-                mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION);
-        File logsDir = compatVersion.getLogsDir();
-        assertThat(logsDir.exists()).isTrue();
-        assertThat(logsDir.isDirectory()).isTrue();
-        assertThat(logsDir.getAbsolutePath())
-                .startsWith(mTestDir.getAbsolutePath() + "/" + TEST_VERSION);
-        File logsListFile = compatVersion.getLogsFile();
-        assertThat(logsListFile.exists()).isTrue();
-        assertThat(logsListFile.getAbsolutePath()).startsWith(logsDir.getAbsolutePath());
-        assertThat(readAsString(logsListFile)).isEqualTo(content);
-    }
-
-    @Test
-    public void testCertificateTransparencyInstaller_versionIsAlreadyInstalled()
-            throws IOException {
-        String existingVersion = "666";
-        String existingContent = "i_was_already_installed_successfully";
-        CompatibilityVersion compatVersion =
-                mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION);
-
-        DirectoryUtils.makeDir(mTestDir);
-        try (InputStream inputStream = asStream(existingContent)) {
-            assertThat(compatVersion.install(inputStream, existingVersion)).isTrue();
-        }
-
-        try (InputStream inputStream = asStream("i_will_be_ignored")) {
-            assertThat(
-                            mCertificateTransparencyInstaller.install(
-                                    TEST_VERSION, inputStream, existingVersion))
-                    .isFalse();
-        }
-
-        assertThat(readAsString(compatVersion.getLogsFile())).isEqualTo(existingContent);
-    }
-
-    private static InputStream asStream(String string) throws IOException {
-        return new ByteArrayInputStream(string.getBytes());
-    }
-
-    private static String readAsString(File file) throws IOException {
-        return new String(new FileInputStream(file).readAllBytes());
-    }
-
-    private static void writeToFile(File file, String string) throws IOException {
-        try (OutputStream out = new FileOutputStream(file)) {
-            out.write(string.getBytes());
-        }
-    }
-}
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java
new file mode 100644
index 0000000..2b8b3cd
--- /dev/null
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.net.ct;
+
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.LOG_LIST_INVALID;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SUCCESS;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.UNKNOWN_STATE;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.VERSION_ALREADY_EXISTS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/** Tests for the {@link CompatibilityVersion}. */
+@RunWith(JUnit4.class)
+public class CompatibilityVersionTest {
+
+    private static final String TEST_VERSION = "v123";
+    private static final long LOG_LIST_TIMESTAMP = 123456789L;
+    private static final String SIGNATURE = "fake_signature";
+
+    private final File mTestDir =
+            InstrumentationRegistry.getInstrumentation().getContext().getFilesDir();
+    private final CompatibilityVersion mCompatVersion =
+            new CompatibilityVersion(
+                    TEST_VERSION, Config.URL_SIGNATURE, Config.URL_LOG_LIST, mTestDir);
+
+    @After
+    public void tearDown() {
+        mCompatVersion.delete();
+    }
+
+    @Test
+    public void testCompatibilityVersion_versionDirectory_setupSuccessful() {
+        File versionDir = mCompatVersion.getVersionDir();
+
+        assertThat(versionDir.exists()).isFalse();
+        assertThat(versionDir.getAbsolutePath()).startsWith(mTestDir.getAbsolutePath());
+        assertThat(versionDir.getAbsolutePath()).endsWith(TEST_VERSION);
+    }
+
+    @Test
+    public void testCompatibilityVersion_symlink_setupSuccessful() {
+        File dirSymlink = mCompatVersion.getLogsDirSymlink();
+
+        assertThat(dirSymlink.exists()).isFalse();
+        assertThat(dirSymlink.getAbsolutePath())
+                .startsWith(mCompatVersion.getVersionDir().getAbsolutePath());
+    }
+
+    @Test
+    public void testCompatibilityVersion_logsFile_setupSuccessful() {
+        File logsFile = mCompatVersion.getLogsFile();
+
+        assertThat(logsFile.exists()).isFalse();
+        assertThat(logsFile.getAbsolutePath())
+                .startsWith(mCompatVersion.getLogsDirSymlink().getAbsolutePath());
+    }
+
+    @Test
+    public void testCompatibilityVersion_installSuccessful_keepsStatusDetails() throws Exception {
+        String version = "i_am_version";
+        JSONObject logList = makeLogList(version, "i_am_content");
+
+        try (InputStream inputStream = asStream(logList)) {
+            assertThat(
+                            mCompatVersion.install(
+                                    inputStream,
+                                    LogListUpdateStatus.builder()
+                                            .setSignature(SIGNATURE)
+                                            .setState(UNKNOWN_STATE)))
+                    .isEqualTo(
+                            LogListUpdateStatus.builder()
+                                    .setSignature(SIGNATURE)
+                                    .setLogListTimestamp(LOG_LIST_TIMESTAMP)
+                                    // Ensure the state is correctly overridden to SUCCESS
+                                    .setState(SUCCESS)
+                                    .build());
+        }
+    }
+
+    @Test
+    public void testCompatibilityVersion_installSuccessful() throws Exception {
+        String version = "i_am_version";
+        JSONObject logList = makeLogList(version, "i_am_content");
+
+        try (InputStream inputStream = asStream(logList)) {
+            assertThat(
+                            mCompatVersion.install(
+                                    inputStream, LogListUpdateStatus.builder()))
+                    .isEqualTo(getSuccessfulUpdateStatus());
+        }
+
+        File logListFile = mCompatVersion.getLogsFile();
+        assertThat(logListFile.exists()).isTrue();
+        assertThat(logListFile.getCanonicalPath())
+                .isEqualTo(
+                        // <path-to-test-files>/v123/logs-i_am_version/log_list.json
+                        new File(
+                                        new File(
+                                                mCompatVersion.getVersionDir(),
+                                                CompatibilityVersion.LOGS_DIR_PREFIX + version),
+                                        CompatibilityVersion.LOGS_LIST_FILE_NAME)
+                                .getCanonicalPath());
+        assertThat(logListFile.getAbsolutePath())
+                .isEqualTo(
+                        // <path-to-test-files>/v123/current/log_list.json
+                        new File(
+                                        new File(
+                                                mCompatVersion.getVersionDir(),
+                                                CompatibilityVersion.CURRENT_LOGS_DIR_SYMLINK_NAME),
+                                        CompatibilityVersion.LOGS_LIST_FILE_NAME)
+                                .getAbsolutePath());
+    }
+
+    @Test
+    public void testCompatibilityVersion_deleteSuccessfully() throws Exception {
+        try (InputStream inputStream = asStream(makeLogList(/* version= */ "123"))) {
+            assertThat(
+                            mCompatVersion.install(
+                                    inputStream, LogListUpdateStatus.builder()))
+                    .isEqualTo(getSuccessfulUpdateStatus());
+        }
+
+        mCompatVersion.delete();
+
+        assertThat(mCompatVersion.getLogsFile().exists()).isFalse();
+    }
+
+    @Test
+    public void testCompatibilityVersion_invalidLogList() throws Exception {
+        try (InputStream inputStream = new ByteArrayInputStream(("not_a_valid_list".getBytes()))) {
+            assertThat(
+                            mCompatVersion.install(
+                                    inputStream, LogListUpdateStatus.builder()))
+                    .isEqualTo(LogListUpdateStatus.builder().setState(LOG_LIST_INVALID).build());
+        }
+
+        assertThat(mCompatVersion.getLogsFile().exists()).isFalse();
+    }
+
+    @Test
+    public void testCompatibilityVersion_incompleteVersionExists_replacesOldVersion()
+            throws Exception {
+        String existingVersion = "666";
+        File existingLogDir =
+                new File(
+                        mCompatVersion.getVersionDir(),
+                        CompatibilityVersion.LOGS_DIR_PREFIX + existingVersion);
+        assertThat(existingLogDir.mkdirs()).isTrue();
+        File logsListFile = new File(existingLogDir, CompatibilityVersion.LOGS_LIST_FILE_NAME);
+        assertThat(logsListFile.createNewFile()).isTrue();
+
+        JSONObject newLogList = makeLogList(existingVersion, "i_am_the_real_content");
+        try (InputStream inputStream = asStream(newLogList)) {
+            assertThat(
+                            mCompatVersion.install(
+                                    inputStream, LogListUpdateStatus.builder()))
+                    .isEqualTo(getSuccessfulUpdateStatus());
+        }
+
+        assertThat(readAsString(logsListFile)).isEqualTo(newLogList.toString());
+    }
+
+    @Test
+    public void testCompatibilityVersion_versionAlreadyExists_installFails() throws Exception {
+        String existingVersion = "666";
+        JSONObject existingLogList = makeLogList(existingVersion, "i_was_installed_successfully");
+        try (InputStream inputStream = asStream(existingLogList)) {
+            assertThat(
+                            mCompatVersion.install(
+                                    inputStream, LogListUpdateStatus.builder()))
+                    .isEqualTo(getSuccessfulUpdateStatus());
+        }
+
+        try (InputStream inputStream = asStream(makeLogList(existingVersion, "i_am_ignored"))) {
+            assertThat(
+                            mCompatVersion.install(
+                                    inputStream, LogListUpdateStatus.builder()))
+                    .isEqualTo(
+                            LogListUpdateStatus.builder()
+                                    .setState(VERSION_ALREADY_EXISTS)
+                                    .setLogListTimestamp(LOG_LIST_TIMESTAMP)
+                                    .build());
+        }
+
+        assertThat(readAsString(mCompatVersion.getLogsFile()))
+                .isEqualTo(existingLogList.toString());
+    }
+
+    private static InputStream asStream(JSONObject logList) throws IOException {
+        return new ByteArrayInputStream(logList.toString().getBytes());
+    }
+
+    private static JSONObject makeLogList(String version) throws JSONException {
+        return new JSONObject()
+                .put("version", version)
+                .put("log_list_timestamp", LOG_LIST_TIMESTAMP);
+    }
+
+    private static JSONObject makeLogList(String version, String content) throws JSONException {
+        return makeLogList(version).put("content", content);
+    }
+
+    private static LogListUpdateStatus getSuccessfulUpdateStatus() {
+        return LogListUpdateStatus.builder()
+                .setState(SUCCESS)
+                .setLogListTimestamp(LOG_LIST_TIMESTAMP)
+                .build();
+    }
+
+    private static String readAsString(File file) throws IOException {
+        try (InputStream in = new FileInputStream(file)) {
+            return new String(in.readAllBytes());
+        }
+    }
+}
diff --git a/remoteauth/OWNERS b/remoteauth/OWNERS
index 25a32b9..ee46c1c 100644
--- a/remoteauth/OWNERS
+++ b/remoteauth/OWNERS
@@ -2,7 +2,6 @@
 # Bug template url: http://b/new?component=1145231&template=1715387
 billyhuang@google.com
 boetger@google.com
-casbor@google.com
 derekjedral@google.com
 dlm@google.com
 igorzas@google.com
diff --git a/remoteauth/service/jni/Android.bp b/remoteauth/service/jni/Android.bp
index 57e3ec9..c7ad738 100644
--- a/remoteauth/service/jni/Android.bp
+++ b/remoteauth/service/jni/Android.bp
@@ -24,9 +24,6 @@
         "libasync_trait",
     ],
     prefer_rlib: true,
-    apex_available: [
-        "com.android.remoteauth",
-    ],
     host_supported: true,
 }
 
diff --git a/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs b/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
index 9add6df..1d43d38 100644
--- a/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
+++ b/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
@@ -140,6 +140,7 @@
 }
 
 impl Platform for JavaPlatform {
+    #[allow(clippy::unit_arg)]
     fn send_request(
         &mut self,
         connection_id: i32,
diff --git a/remoteauth/service/jni/src/unique_jvm.rs b/remoteauth/service/jni/src/unique_jvm.rs
index 46cc361..ddbb16f 100644
--- a/remoteauth/service/jni/src/unique_jvm.rs
+++ b/remoteauth/service/jni/src/unique_jvm.rs
@@ -41,6 +41,7 @@
     Ok(())
 }
 /// Gets a 'static reference to the unique JavaVM. Returns None if set_once() was never called.
+#[allow(static_mut_refs)]
 pub(crate) fn get_static_ref() -> Option<&'static Arc<JavaVM>> {
     // Safety: follows [this pattern](https://doc.rust-lang.org/std/sync/struct.Once.html).
     // Modification to static mut is nested inside call_once.
diff --git a/service-b/Android.bp b/service-b/Android.bp
new file mode 100644
index 0000000..47439ee
--- /dev/null
+++ b/service-b/Android.bp
@@ -0,0 +1,40 @@
+//
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_team: "trendy_team_enigma",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// TODO: b/380331248 This lib is a non-jarjared version of "service-connectivity-b-platform"
+// It will only be included in the Tethering module when the build system flag
+// RELEASE_MOVE_VCN_TO_MAINLINE is enabled. Including "service-connectivity-b-platform"
+// in Tethering will break art branch check because that lib lives in framework/base.
+// Once VCN is moved to Connectivity/, "service-connectivity-b-platform" can be cleaned up.
+java_library {
+    name: "service-connectivity-b-pre-jarjar",
+    defaults: ["service-connectivity-b-pre-jarjar-defaults"],
+    libs: ["service-connectivity-pre-jarjar"],
+
+    sdk_version: "system_server_current",
+
+    // TODO(b/210962470): Bump this to B
+    min_sdk_version: "30",
+
+    apex_available: [
+        "com.android.tethering",
+    ],
+}
diff --git a/service-t/Android.bp b/service-t/Android.bp
index 787e94e..d2e2a80 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -101,7 +101,7 @@
     min_sdk_version: "21",
     lint: {
         error_checks: ["NewApi"],
-
+        baseline_filename: "lint-baseline-service-connectivity-mdns-standalone-build-test.xml",
     },
     srcs: [
         "src/com/android/server/connectivity/mdns/**/*.java",
diff --git a/service-t/lint-baseline-service-connectivity-mdns-standalone-build-test.xml b/service-t/lint-baseline-service-connectivity-mdns-standalone-build-test.xml
new file mode 100644
index 0000000..232d31c
--- /dev/null
+++ b/service-t/lint-baseline-service-connectivity-mdns-standalone-build-test.xml
@@ -0,0 +1,972 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.4.0-alpha08" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha08">
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+            line="37"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+            line="37"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+            line="37"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+            line="37"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+            line="37"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+</issues>
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 0adb290..555549c 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -23,6 +23,7 @@
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
+import static android.net.nsd.AdvertisingRequest.FLAG_SKIP_PROBING;
 import static android.net.nsd.NsdManager.MDNS_DISCOVERY_MANAGER_EVENT;
 import static android.net.nsd.NsdManager.MDNS_SERVICE_EVENT;
 import static android.net.nsd.NsdManager.RESOLVE_SERVICE_SUCCEEDED;
@@ -981,7 +982,7 @@
                                         NsdManager.FAILURE_INTERNAL_ERROR, false /* isLegacy */);
                                 break;
                             }
-                            boolean isUpdateOnly = (advertisingRequest.getAdvertisingConfig()
+                            boolean isUpdateOnly = (advertisingRequest.getFlags()
                                     & AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY) > 0;
                             // If it is an update request, then reuse the old transactionId
                             if (isUpdateOnly) {
@@ -1046,9 +1047,12 @@
 
                             serviceInfo.setSubtypes(subtypes);
                             maybeStartMonitoringSockets();
+                            final boolean skipProbing = (advertisingRequest.getFlags()
+                                    & FLAG_SKIP_PROBING) > 0;
                             final MdnsAdvertisingOptions mdnsAdvertisingOptions =
                                     MdnsAdvertisingOptions.newBuilder()
                                             .setIsOnlyUpdate(isUpdateOnly)
+                                            .setSkipProbing(skipProbing)
                                             .setTtl(advertisingRequest.getTtl())
                                             .build();
                             mAdvertiser.addOrUpdateService(transactionId, serviceInfo,
@@ -1943,6 +1947,8 @@
                 .setCachedServicesRetentionTime(mDeps.getDeviceConfigPropertyInt(
                         MdnsFeatureFlags.NSD_CACHED_SERVICES_RETENTION_TIME,
                         MdnsFeatureFlags.DEFAULT_CACHED_SERVICES_RETENTION_TIME_MILLISECONDS))
+                .setIsShortHostnamesEnabled(mDeps.isTetheringFeatureNotChickenedOut(
+                        mContext, MdnsFeatureFlags.NSD_USE_SHORT_HOSTNAMES))
                 .setOverrideProvider(new MdnsFeatureFlags.FlagOverrideProvider() {
                     @Override
                     public boolean isForceEnabledForTest(@NonNull String flag) {
diff --git a/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java b/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
index f55db93..81ba530 100644
--- a/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
+++ b/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
@@ -139,11 +139,8 @@
                 // Base service type
                 questions.add(new MdnsPointerRecord(serviceTypeLabels, expectUnicastResponse));
                 for (String subtype : subtypes) {
-                    final String[] labels = new String[serviceTypeLabels.length + 2];
-                    labels[0] = MdnsConstants.SUBTYPE_PREFIX + subtype;
-                    labels[1] = MdnsConstants.SUBTYPE_LABEL;
-                    System.arraycopy(serviceTypeLabels, 0, labels, 2, serviceTypeLabels.length);
-
+                    final String[] labels = MdnsUtils.constructFullSubtype(serviceTypeLabels,
+                            MdnsConstants.SUBTYPE_PREFIX + subtype);
                     questions.add(new MdnsPointerRecord(labels, expectUnicastResponse));
                 }
             }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
index 9c52eca..c3306bd 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -46,6 +46,7 @@
 import com.android.server.connectivity.ConnectivityResources;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 
+import java.security.SecureRandom;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -117,7 +118,7 @@
          * Generates a unique hostname to be used by the device.
          */
         @NonNull
-        public String[] generateHostname() {
+        public String[] generateHostname(boolean useShortFormat) {
             // Generate a very-probably-unique hostname. This allows minimizing possible conflicts
             // to the point that probing for it is no longer necessary (as per RFC6762 8.1 last
             // paragraph), and does not leak more information than what could already be obtained by
@@ -127,10 +128,24 @@
             // Having a different hostname per interface is an acceptable option as per RFC6762 14.
             // This hostname will change every time the interface is reconnected, so this does not
             // allow tracking the device.
-            // TODO: consider deriving a hostname from other sources, such as the IPv6 addresses
-            // (reusing the same privacy-protecting mechanics).
-            return new String[] {
-                    "Android_" + UUID.randomUUID().toString().replace("-", ""), LOCAL_TLD };
+            if (useShortFormat) {
+                // A short hostname helps reduce the size of APF mDNS filtering programs, and
+                // is necessary for compatibility with some Matter 1.0 devices which assumed
+                // 16 characters is the maximum length.
+                // Generate a hostname matching Android_[0-9A-Z]{8}, which has 36^8 possibilities.
+                // Even with 100 devices advertising the probability of collision is around 2E-9,
+                // which is negligible.
+                final SecureRandom sr = new SecureRandom();
+                final String allowedChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+                final StringBuilder sb = new StringBuilder(8);
+                for (int i = 0; i < 8; i++) {
+                    sb.append(allowedChars.charAt(sr.nextInt(allowedChars.length())));
+                }
+                return new String[]{ "Android_" + sb.toString(), LOCAL_TLD };
+            } else {
+                return new String[]{
+                        "Android_" + UUID.randomUUID().toString().replace("-", ""), LOCAL_TLD};
+            }
         }
     }
 
@@ -825,7 +840,7 @@
         mCb = cb;
         mSocketProvider = socketProvider;
         mDeps = deps;
-        mDeviceHostName = deps.generateHostname();
+        mDeviceHostName = deps.generateHostname(mDnsFeatureFlags.isShortHostnamesEnabled());
         mSharedLog = sharedLog;
         mMdnsFeatureFlags = mDnsFeatureFlags;
         final ConnectivityResources res = new ConnectivityResources(context);
@@ -943,7 +958,7 @@
         mRegistrations.remove(id);
         // Regenerates host name when registrations removed.
         if (mRegistrations.size() == 0) {
-            mDeviceHostName = mDeps.generateHostname();
+            mDeviceHostName = mDeps.generateHostname(mMdnsFeatureFlags.isShortHostnamesEnabled());
         }
     }
 
@@ -992,6 +1007,19 @@
         });
     }
 
+    private List<String> getOffloadSubtype(@NonNull NsdServiceInfo nsdServiceInfo) {
+        // Workaround: Google Cast doesn't announce subtypes per DNS-SD/mDNS spec.
+        // Thus, subtypes aren't offloaded; only "_googlecast._tcp" is.
+        // Subtype responses occur when hardware offload is off.
+        // This solution works because Google Cast doesn't follow the intended usage of subtypes in
+        // the spec, as it always discovers for both the subtype+base type, and only uses the mDNS
+        // subtype as an optimization.
+        if (nsdServiceInfo.getServiceType().equals("_googlecast._tcp")) {
+            return new ArrayList<>();
+        }
+        return new ArrayList<>(nsdServiceInfo.getSubtypes());
+    }
+
     private OffloadServiceInfoWrapper createOffloadService(int serviceId,
             @NonNull Registration registration, byte[] rawOffloadPacket) {
         final NsdServiceInfo nsdServiceInfo = registration.getServiceInfo();
@@ -1002,7 +1030,7 @@
         final OffloadServiceInfo offloadServiceInfo = new OffloadServiceInfo(
                 new OffloadServiceInfo.Key(nsdServiceInfo.getServiceName(),
                         nsdServiceInfo.getServiceType()),
-                new ArrayList<>(nsdServiceInfo.getSubtypes()),
+                getOffloadSubtype(nsdServiceInfo),
                 String.join(".", mDeviceHostName),
                 rawOffloadPacket,
                 priority,
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java
index a81d1e4..5133d4f 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java
@@ -34,13 +34,15 @@
     private final boolean mIsOnlyUpdate;
     @Nullable
     private final Duration mTtl;
+    private final boolean mSkipProbing;
 
     /**
      * Parcelable constructs for a {@link MdnsAdvertisingOptions}.
      */
-    MdnsAdvertisingOptions(boolean isOnlyUpdate, @Nullable Duration ttl) {
+    MdnsAdvertisingOptions(boolean isOnlyUpdate, @Nullable Duration ttl, boolean skipProbing) {
         this.mIsOnlyUpdate = isOnlyUpdate;
         this.mTtl = ttl;
+        this.mSkipProbing = skipProbing;
     }
 
     /**
@@ -68,6 +70,13 @@
     }
 
     /**
+     * @return {@code true} if the probing step should be skipped.
+     */
+    public boolean skipProbing() {
+        return mSkipProbing;
+    }
+
+    /**
      * Returns the TTL for all records in a service.
      */
     @Nullable
@@ -104,6 +113,7 @@
      */
     public static final class Builder {
         private boolean mIsOnlyUpdate = false;
+        private boolean mSkipProbing = false;
         @Nullable
         private Duration mTtl;
 
@@ -127,10 +137,18 @@
         }
 
         /**
+         * Sets whether to skip the probing step.
+         */
+        public Builder setSkipProbing(boolean skipProbing) {
+            this.mSkipProbing = skipProbing;
+            return this;
+        }
+
+        /**
          * Builds a {@link MdnsAdvertisingOptions} with the arguments supplied to this builder.
          */
         public MdnsAdvertisingOptions build() {
-            return new MdnsAdvertisingOptions(mIsOnlyUpdate, mTtl);
+            return new MdnsAdvertisingOptions(mIsOnlyUpdate, mTtl, mSkipProbing);
         }
     }
 }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
index 4e27fef..2f3bdc5 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
@@ -81,6 +81,12 @@
     public static final String NSD_CACHED_SERVICES_REMOVAL = "nsd_cached_services_removal";
 
     /**
+     * A feature flag to control whether to use shorter (16 characters + .local) hostnames, instead
+     * of Android_[32 characters] hostnames.
+     */
+    public static final String NSD_USE_SHORT_HOSTNAMES = "nsd_use_short_hostnames";
+
+    /**
      * A feature flag to control the retention time for cached services.
      *
      * <p> Making the retention time configurable allows for testing and future adjustments.
@@ -122,6 +128,9 @@
     // Retention Time for cached services
     public final long mCachedServicesRetentionTime;
 
+    // Flag to use shorter (16 characters + .local) hostnames
+    public final boolean mIsShortHostnamesEnabled;
+
     @Nullable
     private final FlagOverrideProvider mOverrideProvider;
 
@@ -217,6 +226,10 @@
                 NSD_CACHED_SERVICES_RETENTION_TIME, (int) mCachedServicesRetentionTime);
     }
 
+    public boolean isShortHostnamesEnabled() {
+        return mIsShortHostnamesEnabled || isForceEnabledForTest(NSD_USE_SHORT_HOSTNAMES);
+    }
+
     /**
      * The constructor for {@link MdnsFeatureFlags}.
      */
@@ -231,6 +244,7 @@
             boolean avoidAdvertisingEmptyTxtRecords,
             boolean isCachedServicesRemovalEnabled,
             long cachedServicesRetentionTime,
+            boolean isShortHostnamesEnabled,
             @Nullable FlagOverrideProvider overrideProvider) {
         mIsMdnsOffloadFeatureEnabled = isOffloadFeatureEnabled;
         mIncludeInetAddressRecordsInProbing = includeInetAddressRecordsInProbing;
@@ -243,6 +257,7 @@
         mAvoidAdvertisingEmptyTxtRecords = avoidAdvertisingEmptyTxtRecords;
         mIsCachedServicesRemovalEnabled = isCachedServicesRemovalEnabled;
         mCachedServicesRetentionTime = cachedServicesRetentionTime;
+        mIsShortHostnamesEnabled = isShortHostnamesEnabled;
         mOverrideProvider = overrideProvider;
     }
 
@@ -266,6 +281,7 @@
         private boolean mAvoidAdvertisingEmptyTxtRecords;
         private boolean mIsCachedServicesRemovalEnabled;
         private long mCachedServicesRetentionTime;
+        private boolean mIsShortHostnamesEnabled;
         private FlagOverrideProvider mOverrideProvider;
 
         /**
@@ -283,6 +299,7 @@
             mAvoidAdvertisingEmptyTxtRecords = true; // Default enabled.
             mIsCachedServicesRemovalEnabled = false;
             mCachedServicesRetentionTime = DEFAULT_CACHED_SERVICES_RETENTION_TIME_MILLISECONDS;
+            mIsShortHostnamesEnabled = true; // Default enabled.
             mOverrideProvider = null;
         }
 
@@ -409,6 +426,16 @@
         }
 
         /**
+         * Set whether the short hostnames feature is enabled.
+         *
+         * @see #NSD_USE_SHORT_HOSTNAMES
+         */
+        public Builder setIsShortHostnamesEnabled(boolean isShortHostnamesEnabled) {
+            mIsShortHostnamesEnabled = isShortHostnamesEnabled;
+            return this;
+        }
+
+        /**
          * Builds a {@link MdnsFeatureFlags} with the arguments supplied to this builder.
          */
         public MdnsFeatureFlags build() {
@@ -423,6 +450,7 @@
                     mAvoidAdvertisingEmptyTxtRecords,
                     mIsCachedServicesRemovalEnabled,
                     mCachedServicesRetentionTime,
+                    mIsShortHostnamesEnabled,
                     mOverrideProvider);
         }
     }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
index 58defa9..b9b09ed 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -122,28 +122,32 @@
         }
         @Override
         public void onFinished(MdnsProber.ProbingInfo info) {
-            final MdnsAnnouncer.AnnouncementInfo announcementInfo;
-            mSharedLog.i("Probing finished for service " + info.getServiceId());
-            mCbHandler.post(() -> mCb.onServiceProbingSucceeded(
-                    MdnsInterfaceAdvertiser.this, info.getServiceId()));
-            try {
-                announcementInfo = mRecordRepository.onProbingSucceeded(info);
-            } catch (IOException e) {
-                mSharedLog.e("Error building announcements", e);
-                return;
-            }
+            handleProbingFinished(info);
+        }
+    }
 
-            mAnnouncer.startSending(info.getServiceId(), announcementInfo,
-                    0L /* initialDelayMs */);
+    private void handleProbingFinished(MdnsProber.ProbingInfo info) {
+        final MdnsAnnouncer.AnnouncementInfo announcementInfo;
+        mSharedLog.i("Probing finished for service " + info.getServiceId());
+        mCbHandler.post(() -> mCb.onServiceProbingSucceeded(
+                MdnsInterfaceAdvertiser.this, info.getServiceId()));
+        try {
+            announcementInfo = mRecordRepository.onProbingSucceeded(info);
+        } catch (IOException e) {
+            mSharedLog.e("Error building announcements", e);
+            return;
+        }
 
-            // Re-announce the services which have the same custom hostname.
-            final String hostname = mRecordRepository.getHostnameForServiceId(info.getServiceId());
-            if (hostname != null) {
-                final List<MdnsAnnouncer.AnnouncementInfo> announcementInfos =
-                        new ArrayList<>(mRecordRepository.restartAnnouncingForHostname(hostname));
-                announcementInfos.removeIf((i) -> i.getServiceId() == info.getServiceId());
-                reannounceServices(announcementInfos);
-            }
+        mAnnouncer.startSending(info.getServiceId(), announcementInfo,
+                0L /* initialDelayMs */);
+
+        // Re-announce the services which have the same custom hostname.
+        final String hostname = mRecordRepository.getHostnameForServiceId(info.getServiceId());
+        if (hostname != null) {
+            final List<MdnsAnnouncer.AnnouncementInfo> announcementInfos =
+                    new ArrayList<>(mRecordRepository.restartAnnouncingForHostname(hostname));
+            announcementInfos.removeIf((i) -> i.getServiceId() == info.getServiceId());
+            reannounceServices(announcementInfos);
         }
     }
 
@@ -280,7 +284,12 @@
                     + " getting re-added, cancelling exit announcements");
             mAnnouncer.stop(replacedExitingService);
         }
-        mProber.startProbing(mRecordRepository.setServiceProbing(id));
+        final MdnsProber.ProbingInfo probingInfo = mRecordRepository.setServiceProbing(id);
+        if (advertisingOptions.skipProbing()) {
+            handleProbingFinished(probingInfo);
+        } else {
+            mProber.startProbing(probingInfo);
+        }
     }
 
     /**
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java b/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
index 356b738..7495aec 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
@@ -16,9 +16,14 @@
 
 package com.android.server.connectivity.mdns;
 
+import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE;
+import static com.android.server.connectivity.mdns.MdnsSearchOptions.PASSIVE_QUERY_MODE;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 /**
  * The query scheduler class for calculating next query tasks parameters.
  * <p>
@@ -26,6 +31,25 @@
  */
 public class MdnsQueryScheduler {
 
+    @VisibleForTesting
+    // RFC 6762 5.2: The interval between the first two queries MUST be at least one second.
+    static final int INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS = 1000;
+    private static final int INITIAL_TIME_BETWEEN_BURSTS_MS =
+            (int) MdnsConfigs.initialTimeBetweenBurstsMs();
+    private static final int MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS =
+            (int) MdnsConfigs.timeBetweenBurstsMs();
+    private static final int QUERIES_PER_BURST = (int) MdnsConfigs.queriesPerBurst();
+    private static final int TIME_BETWEEN_QUERIES_IN_BURST_MS =
+            (int) MdnsConfigs.timeBetweenQueriesInBurstMs();
+    private static final int QUERIES_PER_BURST_PASSIVE_MODE =
+            (int) MdnsConfigs.queriesPerBurstPassive();
+    @VisibleForTesting
+    // Basically this tries to send one query per typical DTIM interval 100ms, to maximize the
+    // chances that a query will be received if devices are using a DTIM multiplier (in which case
+    // they only listen once every [multiplier] DTIM intervals).
+    static final int TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS = 100;
+    static final int MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS = 60000;
+
     /**
      * The argument for tracking the query tasks status.
      */
@@ -72,19 +96,21 @@
         if (mLastScheduledQueryTaskArgs == null) {
             return null;
         }
-        if (!mLastScheduledQueryTaskArgs.config.shouldUseQueryBackoff(numOfQueriesBeforeBackoff)) {
+        final QueryTaskConfig lastConfig = mLastScheduledQueryTaskArgs.config;
+        if (!shouldUseQueryBackoff(lastConfig.queryIndex, lastConfig.queryMode,
+                numOfQueriesBeforeBackoff)) {
             return null;
         }
 
         final long timeToRun = calculateTimeToRun(mLastScheduledQueryTaskArgs,
-                mLastScheduledQueryTaskArgs.config, now, minRemainingTtl, lastSentTime,
+                lastConfig.queryIndex, lastConfig.queryMode, now, minRemainingTtl, lastSentTime,
                 numOfQueriesBeforeBackoff, false /* forceEnableBackoff */);
 
         if (timeToRun <= mLastScheduledQueryTaskArgs.timeToRun) {
             return null;
         }
 
-        mLastScheduledQueryTaskArgs = new ScheduledQueryTaskArgs(mLastScheduledQueryTaskArgs.config,
+        mLastScheduledQueryTaskArgs = new ScheduledQueryTaskArgs(lastConfig,
                 timeToRun,
                 minRemainingTtl + now,
                 sessionId);
@@ -104,17 +130,19 @@
             int queryMode,
             int numOfQueriesBeforeBackoff,
             boolean forceEnableBackoff) {
-        final QueryTaskConfig nextRunConfig = currentConfig.getConfigForNextRun(queryMode);
+        final int newQueryIndex = currentConfig.getConfigForNextRun(queryMode).queryIndex;
         long timeToRun;
         if (mLastScheduledQueryTaskArgs == null && !forceEnableBackoff) {
-            timeToRun = now + nextRunConfig.getDelayBeforeTaskWithoutBackoff();
+            timeToRun = now + getDelayBeforeTaskWithoutBackoff(
+                    newQueryIndex, queryMode);
         } else {
-            timeToRun = calculateTimeToRun(mLastScheduledQueryTaskArgs,
-                    nextRunConfig, now, minRemainingTtl, lastSentTime, numOfQueriesBeforeBackoff,
-                    forceEnableBackoff);
+            timeToRun = calculateTimeToRun(mLastScheduledQueryTaskArgs, newQueryIndex,
+                    queryMode, now, minRemainingTtl, lastSentTime,
+                    numOfQueriesBeforeBackoff, forceEnableBackoff);
         }
-        mLastScheduledQueryTaskArgs = new ScheduledQueryTaskArgs(nextRunConfig, timeToRun,
-                minRemainingTtl + now,
+        mLastScheduledQueryTaskArgs = new ScheduledQueryTaskArgs(
+                currentConfig.getConfigForNextRun(queryMode),
+                timeToRun, minRemainingTtl + now,
                 sessionId);
         return mLastScheduledQueryTaskArgs;
     }
@@ -131,11 +159,11 @@
     }
 
     private static long calculateTimeToRun(@Nullable ScheduledQueryTaskArgs taskArgs,
-            QueryTaskConfig queryTaskConfig, long now, long minRemainingTtl, long lastSentTime,
+            int queryIndex, int queryMode, long now, long minRemainingTtl, long lastSentTime,
             int numOfQueriesBeforeBackoff, boolean forceEnableBackoff) {
-        final long baseDelayInMs = queryTaskConfig.getDelayBeforeTaskWithoutBackoff();
+        final long baseDelayInMs = getDelayBeforeTaskWithoutBackoff(queryIndex, queryMode);
         if (!(forceEnableBackoff
-                || queryTaskConfig.shouldUseQueryBackoff(numOfQueriesBeforeBackoff))) {
+                || shouldUseQueryBackoff(queryIndex, queryMode, numOfQueriesBeforeBackoff))) {
             return lastSentTime + baseDelayInMs;
         }
         if (minRemainingTtl <= 0) {
@@ -152,4 +180,93 @@
         }
         return Math.max(now + (long) (0.8 * minRemainingTtl), lastSentTime + baseDelayInMs);
     }
+
+    private static int getBurstIndex(int queryIndex, int queryMode) {
+        if (queryMode == PASSIVE_QUERY_MODE && queryIndex >= QUERIES_PER_BURST) {
+            // In passive mode, after the first burst of QUERIES_PER_BURST queries, subsequent
+            // bursts have QUERIES_PER_BURST_PASSIVE_MODE queries.
+            final int queryIndexAfterFirstBurst = queryIndex - QUERIES_PER_BURST;
+            return 1 + (queryIndexAfterFirstBurst / QUERIES_PER_BURST_PASSIVE_MODE);
+        } else {
+            return queryIndex / QUERIES_PER_BURST;
+        }
+    }
+
+    private static int getQueryIndexInBurst(int queryIndex, int queryMode) {
+        if (queryMode == PASSIVE_QUERY_MODE && queryIndex >= QUERIES_PER_BURST) {
+            final int queryIndexAfterFirstBurst = queryIndex - QUERIES_PER_BURST;
+            return queryIndexAfterFirstBurst % QUERIES_PER_BURST_PASSIVE_MODE;
+        } else {
+            return queryIndex % QUERIES_PER_BURST;
+        }
+    }
+
+    private static boolean isFirstBurst(int queryIndex, int queryMode) {
+        return getBurstIndex(queryIndex, queryMode) == 0;
+    }
+
+    static boolean isFirstQueryInBurst(int queryIndex, int queryMode) {
+        return getQueryIndexInBurst(queryIndex, queryMode) == 0;
+    }
+
+    private static long getDelayBeforeTaskWithoutBackoff(int queryIndex, int queryMode) {
+        final int burstIndex = getBurstIndex(queryIndex, queryMode);
+        final int queryIndexInBurst = getQueryIndexInBurst(queryIndex, queryMode);
+        if (queryIndexInBurst == 0) {
+            return getTimeToBurstMs(burstIndex, queryMode);
+        } else if (queryIndexInBurst == 1 && queryMode == AGGRESSIVE_QUERY_MODE) {
+            // In aggressive mode, the first 2 queries are sent without delay.
+            return 0;
+        }
+        return queryMode == AGGRESSIVE_QUERY_MODE
+                ? TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS
+                : TIME_BETWEEN_QUERIES_IN_BURST_MS;
+    }
+
+    /**
+     * Shifts a value left by the specified number of bits, coercing to at most maxValue.
+     *
+     * <p>This allows calculating min(value*2^shift, maxValue) without overflow.
+     */
+    private static int boundedLeftShift(int value, int shift, int maxValue) {
+        // There must be at least one leading zero for positive values, so the maximum left shift
+        // without overflow is the number of leading zeros minus one.
+        final int maxShift = Integer.numberOfLeadingZeros(value) - 1;
+        if (shift > maxShift) {
+            // The shift would overflow positive integers, so is greater than maxValue.
+            return maxValue;
+        }
+        return Math.min(value << shift, maxValue);
+    }
+
+    private static int getTimeToBurstMs(int burstIndex, int queryMode) {
+        if (burstIndex == 0) {
+            // No delay before the first burst
+            return 0;
+        }
+        switch (queryMode) {
+            case PASSIVE_QUERY_MODE:
+                return MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS;
+            case AGGRESSIVE_QUERY_MODE:
+                return boundedLeftShift(INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS,
+                        burstIndex - 1,
+                        MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS);
+            default: // ACTIVE_QUERY_MODE
+                return boundedLeftShift(INITIAL_TIME_BETWEEN_BURSTS_MS,
+                        burstIndex - 1,
+                        MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS);
+        }
+    }
+
+    /**
+     * Determine if the query backoff should be used.
+     */
+    public static boolean shouldUseQueryBackoff(int queryIndex, int queryMode,
+            int numOfQueriesBeforeBackoff) {
+        // Don't enable backoff mode during the burst or in the first burst
+        if (!isFirstQueryInBurst(queryIndex, queryMode) || isFirstBurst(queryIndex, queryMode)) {
+            return false;
+        }
+        return queryIndex > numOfQueriesBeforeBackoff;
+    }
 }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
index c3cb776..bfef5d9 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -1482,22 +1482,14 @@
 
     private static String[] splitFullyQualifiedName(
             @NonNull NsdServiceInfo info, @NonNull String[] serviceType) {
-        final String[] split = new String[serviceType.length + 1];
-        split[0] = info.getServiceName();
-        System.arraycopy(serviceType, 0, split, 1, serviceType.length);
-
-        return split;
+        return CollectionUtils.prependArray(String.class, serviceType, info.getServiceName());
     }
 
     private static String[] splitServiceType(@NonNull NsdServiceInfo info) {
         // String.split(pattern, 0) removes trailing empty strings, which would appear when
         // splitting "domain.name." (with a dot a the end), so this is what is needed here.
         final String[] split = info.getServiceType().split("\\.", 0);
-        final String[] type = new String[split.length + 1];
-        System.arraycopy(split, 0, type, 0, split.length);
-        type[split.length] = LOCAL_TLD;
-
-        return type;
+        return CollectionUtils.appendArray(String.class, split, LOCAL_TLD);
     }
 
     /** Returns whether there will be an SRV record when registering the {@code info}. */
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
index a43486e..8c86fb8 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -730,7 +730,7 @@
                                 serviceType,
                                 subtypes,
                                 taskArgs.config.expectUnicastResponse,
-                                taskArgs.config.transactionId,
+                                taskArgs.config.getTransactionId(),
                                 socketKey,
                                 onlyUseIpv6OnIpv6OnlyNetworks,
                                 sendDiscoveryQueries,
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java
index b640c32..d91bd11 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java
@@ -26,6 +26,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresApi;
+import android.annotation.SuppressLint;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -164,6 +165,7 @@
         this(context, looper, new Dependencies(), sharedLog, socketRequestMonitor);
     }
 
+    @SuppressLint("NewApi")
     MdnsSocketProvider(@NonNull Context context, @NonNull Looper looper,
             @NonNull Dependencies deps, @NonNull SharedLog sharedLog,
             @NonNull SocketRequestMonitor socketRequestMonitor) {
@@ -335,6 +337,7 @@
     }
 
     /*** Start monitoring sockets by listening callbacks for sockets creation or removal */
+    @SuppressLint("NewApi")
     public void startMonitoringSockets() {
         ensureRunningOnHandlerThread(mHandler);
         mRequestStop = false; // Reset stop request flag.
@@ -365,6 +368,7 @@
         }
     }
 
+    @SuppressLint("NewApi")
     private void maybeStopMonitoringSockets() {
         if (!mMonitoringSockets) return; // Already unregistered.
         if (!mRequestStop) return; // No stop request.
@@ -560,7 +564,6 @@
             // Never try mDNS on cellular, or on interfaces with incompatible flags
             if (CollectionUtils.contains(transports, TRANSPORT_CELLULAR)
                     || iface.isLoopback()
-                    || iface.isPointToPoint()
                     || iface.isVirtual()
                     || !iface.isUp()) {
                 return false;
diff --git a/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java b/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
index dd4073f..2ac5b74 100644
--- a/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
+++ b/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
@@ -17,7 +17,6 @@
 package com.android.server.connectivity.mdns;
 
 import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE;
-import static com.android.server.connectivity.mdns.MdnsSearchOptions.PASSIVE_QUERY_MODE;
 
 import com.android.internal.annotations.VisibleForTesting;
 
@@ -26,136 +25,22 @@
  * Call to getConfigForNextRun returns a config that can be used to build the next query task.
  */
 public class QueryTaskConfig {
-
-    private static final int INITIAL_TIME_BETWEEN_BURSTS_MS =
-            (int) MdnsConfigs.initialTimeBetweenBurstsMs();
-    private static final int MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS =
-            (int) MdnsConfigs.timeBetweenBurstsMs();
-    private static final int QUERIES_PER_BURST = (int) MdnsConfigs.queriesPerBurst();
-    private static final int TIME_BETWEEN_QUERIES_IN_BURST_MS =
-            (int) MdnsConfigs.timeBetweenQueriesInBurstMs();
-    private static final int QUERIES_PER_BURST_PASSIVE_MODE =
-            (int) MdnsConfigs.queriesPerBurstPassive();
     private static final int UNSIGNED_SHORT_MAX_VALUE = 65536;
-    @VisibleForTesting
-    // RFC 6762 5.2: The interval between the first two queries MUST be at least one second.
-    static final int INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS = 1000;
-    @VisibleForTesting
-    // Basically this tries to send one query per typical DTIM interval 100ms, to maximize the
-    // chances that a query will be received if devices are using a DTIM multiplier (in which case
-    // they only listen once every [multiplier] DTIM intervals).
-    static final int TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS = 100;
-    static final int MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS = 60000;
     private final boolean alwaysAskForUnicastResponse =
             MdnsConfigs.alwaysAskForUnicastResponseInEachBurst();
     @VisibleForTesting
-    final int transactionId;
-    @VisibleForTesting
     final boolean expectUnicastResponse;
-    private final int queryIndex;
-    private final int queryMode;
+    final int queryIndex;
+    final int queryMode;
 
-    QueryTaskConfig(int queryMode, int queryIndex, int transactionId,
-            boolean expectUnicastResponse) {
+    QueryTaskConfig(int queryMode, int queryIndex) {
         this.queryMode = queryMode;
-        this.transactionId = transactionId;
         this.queryIndex = queryIndex;
-        this.expectUnicastResponse = expectUnicastResponse;
+        this.expectUnicastResponse = getExpectUnicastResponse();
     }
 
     QueryTaskConfig(int queryMode) {
-        this(queryMode, 0, 1, true);
-    }
-
-    private static int getBurstIndex(int queryIndex, int queryMode) {
-        if (queryMode == PASSIVE_QUERY_MODE && queryIndex >= QUERIES_PER_BURST) {
-            // In passive mode, after the first burst of QUERIES_PER_BURST queries, subsequent
-            // bursts have QUERIES_PER_BURST_PASSIVE_MODE queries.
-            final int queryIndexAfterFirstBurst = queryIndex - QUERIES_PER_BURST;
-            return 1 + (queryIndexAfterFirstBurst / QUERIES_PER_BURST_PASSIVE_MODE);
-        } else {
-            return queryIndex / QUERIES_PER_BURST;
-        }
-    }
-
-    private static int getQueryIndexInBurst(int queryIndex, int queryMode) {
-        if (queryMode == PASSIVE_QUERY_MODE && queryIndex >= QUERIES_PER_BURST) {
-            final int queryIndexAfterFirstBurst = queryIndex - QUERIES_PER_BURST;
-            return queryIndexAfterFirstBurst % QUERIES_PER_BURST_PASSIVE_MODE;
-        } else {
-            return queryIndex % QUERIES_PER_BURST;
-        }
-    }
-
-    private static boolean isFirstBurst(int queryIndex, int queryMode) {
-        return getBurstIndex(queryIndex, queryMode) == 0;
-    }
-
-    private static boolean isFirstQueryInBurst(int queryIndex, int queryMode) {
-        return getQueryIndexInBurst(queryIndex, queryMode) == 0;
-    }
-
-    // TODO: move delay calculations to MdnsQueryScheduler
-    long getDelayBeforeTaskWithoutBackoff() {
-        return getDelayBeforeTaskWithoutBackoff(queryIndex, queryMode);
-    }
-
-    private static long getDelayBeforeTaskWithoutBackoff(int queryIndex, int queryMode) {
-        final int burstIndex = getBurstIndex(queryIndex, queryMode);
-        final int queryIndexInBurst = getQueryIndexInBurst(queryIndex, queryMode);
-        if (queryIndexInBurst == 0) {
-            return getTimeToBurstMs(burstIndex, queryMode);
-        } else if (queryIndexInBurst == 1 && queryMode == AGGRESSIVE_QUERY_MODE) {
-            // In aggressive mode, the first 2 queries are sent without delay.
-            return 0;
-        }
-        return queryMode == AGGRESSIVE_QUERY_MODE
-                ? TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS
-                : TIME_BETWEEN_QUERIES_IN_BURST_MS;
-    }
-
-    private boolean getExpectUnicastResponse(int queryIndex, int queryMode) {
-        if (queryMode == AGGRESSIVE_QUERY_MODE) {
-            if (isFirstQueryInBurst(queryIndex, queryMode)) {
-                return true;
-            }
-        }
-        return alwaysAskForUnicastResponse;
-    }
-
-    /**
-     * Shifts a value left by the specified number of bits, coercing to at most maxValue.
-     *
-     * <p>This allows calculating min(value*2^shift, maxValue) without overflow.
-     */
-    private static int boundedLeftShift(int value, int shift, int maxValue) {
-        // There must be at least one leading zero for positive values, so the maximum left shift
-        // without overflow is the number of leading zeros minus one.
-        final int maxShift = Integer.numberOfLeadingZeros(value) - 1;
-        if (shift > maxShift) {
-            // The shift would overflow positive integers, so is greater than maxValue.
-            return maxValue;
-        }
-        return Math.min(value << shift, maxValue);
-    }
-
-    private static int getTimeToBurstMs(int burstIndex, int queryMode) {
-        if (burstIndex == 0) {
-            // No delay before the first burst
-            return 0;
-        }
-        switch (queryMode) {
-            case PASSIVE_QUERY_MODE:
-                return MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS;
-            case AGGRESSIVE_QUERY_MODE:
-                return boundedLeftShift(INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS,
-                        burstIndex - 1,
-                        MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS);
-            default: // ACTIVE_QUERY_MODE
-                return boundedLeftShift(INITIAL_TIME_BETWEEN_BURSTS_MS,
-                        burstIndex - 1,
-                        MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS);
-        }
+        this(queryMode, 0);
     }
 
     /**
@@ -163,23 +48,19 @@
      */
     public QueryTaskConfig getConfigForNextRun(int queryMode) {
         final int newQueryIndex = queryIndex + 1;
-        int newTransactionId = transactionId + 1;
-        if (newTransactionId > UNSIGNED_SHORT_MAX_VALUE) {
-            newTransactionId = 1;
-        }
-
-        return new QueryTaskConfig(queryMode, newQueryIndex, newTransactionId,
-                getExpectUnicastResponse(newQueryIndex, queryMode));
+        return new QueryTaskConfig(queryMode, newQueryIndex);
     }
 
-    /**
-     * Determine if the query backoff should be used.
-     */
-    public boolean shouldUseQueryBackoff(int numOfQueriesBeforeBackoff) {
-        // Don't enable backoff mode during the burst or in the first burst
-        if (!isFirstQueryInBurst(queryIndex, queryMode) || isFirstBurst(queryIndex, queryMode)) {
-            return false;
+    public int getTransactionId() {
+        return (queryIndex % (UNSIGNED_SHORT_MAX_VALUE - 1)) + 1;
+    }
+
+    private boolean getExpectUnicastResponse() {
+        if (queryMode == AGGRESSIVE_QUERY_MODE) {
+            if (MdnsQueryScheduler.isFirstQueryInBurst(queryIndex, queryMode)) {
+                return true;
+            }
         }
-        return queryIndex > numOfQueriesBeforeBackoff;
+        return queryIndex == 0 || alwaysAskForUnicastResponse;
     }
 }
diff --git a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
index 41b15dd..282ca9a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
+++ b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
@@ -28,6 +28,7 @@
 import android.util.ArraySet;
 import android.util.Pair;
 
+import com.android.net.module.util.CollectionUtils;
 import com.android.server.connectivity.mdns.MdnsConstants;
 import com.android.server.connectivity.mdns.MdnsInetAddressRecord;
 import com.android.server.connectivity.mdns.MdnsPacket;
@@ -273,11 +274,8 @@
      * of ["_printer", "_sub", "_http", "_tcp"].
      */
     public static String[] constructFullSubtype(String[] serviceType, String subtype) {
-        String[] fullSubtype = new String[serviceType.length + 2];
-        fullSubtype[0] = subtype;
-        fullSubtype[1] = MdnsConstants.SUBTYPE_LABEL;
-        System.arraycopy(serviceType, 0, fullSubtype, 2, serviceType.length);
-        return fullSubtype;
+        return CollectionUtils.prependArray(String.class, serviceType, subtype,
+                MdnsConstants.SUBTYPE_LABEL);
     }
 
     /** A wrapper class of {@link SystemClock} to be mocked in unit tests. */
diff --git a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
index b8689d6..21b9b1d 100644
--- a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
+++ b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
@@ -317,6 +317,6 @@
     @Override
     public List<String> getInterfaceList() {
         PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG);
-        return mTracker.getInterfaceList();
+        return mTracker.getEthernetInterfaceList();
     }
 }
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 5228aab..9b3c7ba 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -21,6 +21,7 @@
 import static android.net.TestNetworkManager.TEST_TAP_PREFIX;
 
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+import static com.android.net.module.util.netlink.NetlinkConstants.IFF_UP;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -40,7 +41,6 @@
 import android.os.Handler;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
-import android.os.ServiceSpecificException;
 import android.system.OsConstants;
 import android.text.TextUtils;
 import android.util.ArrayMap;
@@ -51,18 +51,21 @@
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.HandlerUtils;
 import com.android.net.module.util.NetdUtils;
-import com.android.net.module.util.PermissionUtils;
 import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.ip.NetlinkMonitor;
 import com.android.net.module.util.netlink.NetlinkConstants;
 import com.android.net.module.util.netlink.NetlinkMessage;
+import com.android.net.module.util.netlink.NetlinkUtils;
 import com.android.net.module.util.netlink.RtNetlinkLinkMessage;
 import com.android.net.module.util.netlink.StructIfinfoMsg;
 import com.android.server.connectivity.ConnectivityResources;
 
 import java.io.FileDescriptor;
 import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
 import java.util.ArrayList;
+import java.util.Enumeration;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Objects;
@@ -105,7 +108,7 @@
 
     /**
      * Track test interfaces if true, don't track otherwise.
-     * Volatile is needed as getInterfaceList() does not run on the handler thread.
+     * Volatile is needed as getEthernetInterfaceList() does not run on the handler thread.
      */
     private volatile boolean mIncludeTestInterfaces = false;
 
@@ -394,30 +397,41 @@
         return mFactory.hasInterface(iface);
     }
 
+    private List<String> getAllInterfaces() {
+        final ArrayList<String> interfaces = new ArrayList<>(
+                List.of(mFactory.getAvailableInterfaces(/* includeRestricted */ true)));
+
+        if (mTetheringInterfaceMode == INTERFACE_MODE_SERVER && mTetheringInterface != null) {
+            interfaces.add(mTetheringInterface);
+        }
+        return interfaces;
+    }
+
     String[] getClientModeInterfaces(boolean includeRestricted) {
         return mFactory.getAvailableInterfaces(includeRestricted);
     }
 
-    List<String> getInterfaceList() {
+    List<String> getEthernetInterfaceList() {
         final List<String> interfaceList = new ArrayList<String>();
-        final String[] ifaces;
+        final Enumeration<NetworkInterface> ifaces;
         try {
-            ifaces = mNetd.interfaceGetList();
-        } catch (RemoteException e) {
-            Log.e(TAG, "Could not get list of interfaces " + e);
+            ifaces = NetworkInterface.getNetworkInterfaces();
+        } catch (SocketException e) {
+            Log.e(TAG, "Failed to get ethernet interfaces: ", e);
             return interfaceList;
         }
 
         // There is a possible race with setIncludeTestInterfaces() which can affect
         // isValidEthernetInterface (it returns true for test interfaces if setIncludeTestInterfaces
         // is set to true).
-        // setIncludeTestInterfaces() is only used in tests, and since getInterfaceList() does not
-        // run on the handler thread, the behavior around setIncludeTestInterfaces() is
+        // setIncludeTestInterfaces() is only used in tests, and since getEthernetInterfaceList()
+        // does not run on the handler thread, the behavior around setIncludeTestInterfaces() is
         // indeterminate either way. This can easily be circumvented by waiting on a callback from
         // a test interface after calling setIncludeTestInterfaces() before calling this function.
         // In production code, this has no effect.
-        for (String iface : ifaces) {
-            if (isValidEthernetInterface(iface)) interfaceList.add(iface);
+        while (ifaces.hasMoreElements()) {
+            NetworkInterface iface = ifaces.nextElement();
+            if (isValidEthernetInterface(iface.getName())) interfaceList.add(iface.getName());
         }
         return interfaceList;
     }
@@ -455,10 +469,16 @@
     public void setIncludeTestInterfaces(boolean include) {
         mHandler.post(() -> {
             mIncludeTestInterfaces = include;
-            if (!include) {
+            if (include) {
+                trackAvailableInterfaces();
+            } else {
                 removeTestData();
+                // remove all test interfaces
+                for (String iface : getAllInterfaces()) {
+                    if (isValidEthernetInterface(iface)) continue;
+                    stopTrackingInterface(iface);
+                }
             }
-            trackAvailableInterfaces();
         });
     }
 
@@ -589,18 +609,11 @@
         InterfaceConfigurationParcel config = null;
         // Bring up the interface so we get link status indications.
         try {
-            PermissionUtils.enforceNetworkStackPermission(mContext);
             // Read the flags before attempting to bring up the interface. If the interface is
             // already running an UP event is created after adding the interface.
             config = NetdUtils.getInterfaceConfigParcel(mNetd, iface);
-            // Only bring the interface up when ethernet is enabled.
-            if (mIsEthernetEnabled) {
-                // As a side-effect, NetdUtils#setInterfaceUp() also clears the interface's IPv4
-                // address and readds it which *could* lead to unexpected behavior in the future.
-                NetdUtils.setInterfaceUp(mNetd, iface);
-            } else {
-                NetdUtils.setInterfaceDown(mNetd, iface);
-            }
+            // Only bring the interface up when ethernet is enabled, otherwise set interface down.
+            setInterfaceUpState(iface, mIsEthernetEnabled);
         } catch (IllegalStateException e) {
             // Either the system is crashing or the interface has disappeared. Just ignore the
             // error; we haven't modified any state because we only do that if our calls succeed.
@@ -626,7 +639,7 @@
             nc = mNetworkCapabilities.get(hwAddress);
             if (nc == null) {
                 final boolean isTestIface = iface.matches(TEST_IFACE_REGEXP);
-                nc = createDefaultNetworkCapabilities(isTestIface);
+                nc = createDefaultNetworkCapabilities(isTestIface, /* overrideTransport */ null);
             }
         }
 
@@ -660,15 +673,7 @@
             return;
         }
 
-        if (up) {
-            // WARNING! setInterfaceUp() clears the IPv4 address and readds it. Calling
-            // enableInterface() on an active interface can lead to a provisioning failure which
-            // will cause IpClient to be restarted.
-            // TODO: use netlink directly rather than calling into netd.
-            NetdUtils.setInterfaceUp(mNetd, iface);
-        } else {
-            NetdUtils.setInterfaceDown(mNetd, iface);
-        }
+        setInterfaceUpState(iface, up);
         cb.onResult(iface);
     }
 
@@ -707,10 +712,6 @@
     }
 
     private void maybeTrackInterface(String iface) {
-        if (!isValidEthernetInterface(iface)) {
-            return;
-        }
-
         // If we don't already track this interface, and if this interface matches
         // our regex, start tracking it.
         if (mFactory.hasInterface(iface) || iface.equals(mTetheringInterface)) {
@@ -730,13 +731,9 @@
     }
 
     private void trackAvailableInterfaces() {
-        try {
-            final String[] ifaces = mNetd.interfaceGetList();
-            for (String iface : ifaces) {
-                maybeTrackInterface(iface);
-            }
-        } catch (RemoteException | ServiceSpecificException e) {
-            Log.e(TAG, "Could not get list of interfaces " + e);
+        final List<String> ifaces = getEthernetInterfaceList();
+        for (String iface : ifaces) {
+            maybeTrackInterface(iface);
         }
     }
 
@@ -757,9 +754,13 @@
      */
     private void parseEthernetConfig(String configString) {
         final EthernetTrackerConfig config = createEthernetTrackerConfig(configString);
-        NetworkCapabilities nc = createNetworkCapabilities(
-                !TextUtils.isEmpty(config.mCapabilities)  /* clear default capabilities */,
-                config.mCapabilities, config.mTransport).build();
+        NetworkCapabilities nc;
+        if (TextUtils.isEmpty(config.mCapabilities)) {
+            boolean isTestIface = config.mIface.matches(TEST_IFACE_REGEXP);
+            nc = createDefaultNetworkCapabilities(isTestIface, config.mTransport);
+        } else {
+            nc = createNetworkCapabilities(config.mCapabilities, config.mTransport).build();
+        }
         mNetworkCapabilities.put(config.mIface, nc);
 
         if (null != config.mIpConfig) {
@@ -774,15 +775,16 @@
         return new EthernetTrackerConfig(configString.split(";", /* limit of tokens */ 4));
     }
 
-    private static NetworkCapabilities createDefaultNetworkCapabilities(boolean isTestIface) {
-        NetworkCapabilities.Builder builder = createNetworkCapabilities(
-                false /* clear default capabilities */, null, null)
-                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
-                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
-                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
-                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED)
-                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
-                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
+    private static NetworkCapabilities createDefaultNetworkCapabilities(
+            boolean isTestIface, @Nullable String overrideTransport) {
+        NetworkCapabilities.Builder builder =
+                createNetworkCapabilities(/* commaSeparatedCapabilities */ null, overrideTransport)
+                        .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+                        .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
+                        .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
+                        .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED)
+                        .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
+                        .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
 
         if (isTestIface) {
             builder.addTransportType(NetworkCapabilities.TRANSPORT_TEST);
@@ -796,7 +798,6 @@
     /**
      * Parses a static list of network capabilities
      *
-     * @param clearDefaultCapabilities Indicates whether or not to clear any default capabilities
      * @param commaSeparatedCapabilities A comma separated string list of integer encoded
      *                                   NetworkCapability.NET_CAPABILITY_* values
      * @param overrideTransport A string representing a single integer encoded override transport
@@ -806,12 +807,12 @@
      */
     @VisibleForTesting
     static NetworkCapabilities.Builder createNetworkCapabilities(
-            boolean clearDefaultCapabilities, @Nullable String commaSeparatedCapabilities,
-            @Nullable String overrideTransport) {
+            @Nullable String commaSeparatedCapabilities, @Nullable String overrideTransport) {
 
-        final NetworkCapabilities.Builder builder = clearDefaultCapabilities
-                ? NetworkCapabilities.Builder.withoutDefaultCapabilities()
-                : new NetworkCapabilities.Builder();
+        final NetworkCapabilities.Builder builder =
+                TextUtils.isEmpty(commaSeparatedCapabilities)
+                        ? new NetworkCapabilities.Builder()
+                        : NetworkCapabilities.Builder.withoutDefaultCapabilities();
 
         // Determine the transport type. If someone has tried to define an override transport then
         // attempt to add it. Since we can only have one override, all errors with it will
@@ -967,22 +968,8 @@
             if (mIsEthernetEnabled == enabled) return;
 
             mIsEthernetEnabled = enabled;
-
-            // Interface in server mode should also be included.
-            ArrayList<String> interfaces =
-                    new ArrayList<>(
-                    List.of(mFactory.getAvailableInterfaces(/* includeRestricted */ true)));
-
-            if (mTetheringInterfaceMode == INTERFACE_MODE_SERVER) {
-                interfaces.add(mTetheringInterface);
-            }
-
-            for (String iface : interfaces) {
-                if (enabled) {
-                    NetdUtils.setInterfaceUp(mNetd, iface);
-                } else {
-                    NetdUtils.setInterfaceDown(mNetd, iface);
-                }
+            for (String iface : getAllInterfaces()) {
+                setInterfaceUpState(iface, enabled);
             }
             broadcastEthernetStateChange(mIsEthernetEnabled);
         });
@@ -1016,6 +1003,12 @@
         mListeners.finishBroadcast();
     }
 
+    private void setInterfaceUpState(@NonNull String interfaceName, boolean up) {
+        if (!NetlinkUtils.setInterfaceFlags(interfaceName, up ? IFF_UP : ~IFF_UP)) {
+            Log.e(TAG, "Failed to set interface " + interfaceName + (up ? " up" : " down"));
+        }
+    }
+
     void dump(FileDescriptor fd, IndentingPrintWriter pw, String[] args) {
         postAndWaitForRunnable(() -> {
             pw.println(getClass().getSimpleName());
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index fb712a1..5c5f4ca 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -183,15 +183,17 @@
 import com.android.net.module.util.NetworkStatsUtils;
 import com.android.net.module.util.PermissionUtils;
 import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.SkDestroyListener;
 import com.android.net.module.util.Struct;
 import com.android.net.module.util.Struct.S32;
 import com.android.net.module.util.Struct.U8;
 import com.android.net.module.util.bpf.CookieTagMapKey;
 import com.android.net.module.util.bpf.CookieTagMapValue;
+import com.android.net.module.util.netlink.InetDiagMessage;
+import com.android.net.module.util.netlink.StructInetDiagSockId;
 import com.android.networkstack.apishim.BroadcastOptionsShimImpl;
 import com.android.networkstack.apishim.ConstantsShim;
 import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
-import com.android.server.BpfNetMaps;
 import com.android.server.connectivity.ConnectivityResources;
 
 import java.io.File;
@@ -216,6 +218,7 @@
 import java.util.concurrent.Executor;
 import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
 
 /**
  * Collect and persist detailed network statistics, and provide this data to
@@ -493,7 +496,8 @@
     @Nullable
     private final TrafficStatsRateLimitCache mTrafficStatsUidCache;
     // A feature flag to control whether the client-side rate limit cache should be enabled.
-    static final String TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG =
+    @VisibleForTesting
+    public static final String TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG =
             "trafficstats_client_rate_limit_cache_enabled_flag";
     static final String TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG =
             "trafficstats_rate_limit_cache_enabled_flag";
@@ -725,12 +729,14 @@
             mTrafficStatsUidCache = null;
         }
 
-        // TODO: Remove bpfNetMaps creation and always start SkDestroyListener
-        // Following code is for the experiment to verify the SkDestroyListener refactoring. Based
-        // on the experiment flag, BpfNetMaps starts C SkDestroyListener (existing code) or
-        // NetworkStatsService starts Java SkDestroyListener (new code).
-        final BpfNetMaps bpfNetMaps = mDeps.makeBpfNetMaps(mContext);
-        mSkDestroyListener = mDeps.makeSkDestroyListener(mCookieTagMap, mHandler);
+        mSkDestroyListener = mDeps.makeSkDestroyListener((message) -> {
+            final StructInetDiagSockId sockId = message.inetDiagMsg.id;
+            try {
+                mCookieTagMap.deleteEntry(new CookieTagMapKey(sockId.cookie));
+            } catch (ErrnoException e) {
+                Log.e(TAG, "Failed to delete CookieTagMap entry for " + sockId.cookie  + ": " + e);
+            }
+        }, mHandler);
         mHandler.post(mSkDestroyListener::start);
     }
 
@@ -951,16 +957,11 @@
             return Build.isDebuggable();
         }
 
-        /** Create a new BpfNetMaps. */
-        public BpfNetMaps makeBpfNetMaps(Context ctx) {
-            return new BpfNetMaps(ctx);
-        }
-
         /** Create a new SkDestroyListener. */
-        public SkDestroyListener makeSkDestroyListener(
-                IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap, Handler handler) {
-            return new SkDestroyListener(
-                    cookieTagMap, handler, new SharedLog(MAX_SOCKET_DESTROY_LISTENER_LOGS, TAG));
+        public SkDestroyListener makeSkDestroyListener(Consumer<InetDiagMessage> consumer,
+                Handler handler) {
+            return SkDestroyListener.makeSkDestroyListener(consumer, handler,
+                    new SharedLog(MAX_SOCKET_DESTROY_LISTENER_LOGS, TAG));
         }
 
         /**
diff --git a/service-t/src/com/android/server/net/SkDestroyListener.java b/service-t/src/com/android/server/net/SkDestroyListener.java
deleted file mode 100644
index a6cc2b5..0000000
--- a/service-t/src/com/android/server/net/SkDestroyListener.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.net;
-
-import static android.system.OsConstants.NETLINK_INET_DIAG;
-
-import android.os.Handler;
-import android.system.ErrnoException;
-
-import com.android.net.module.util.IBpfMap;
-import com.android.net.module.util.SharedLog;
-import com.android.net.module.util.bpf.CookieTagMapKey;
-import com.android.net.module.util.bpf.CookieTagMapValue;
-import com.android.net.module.util.ip.NetlinkMonitor;
-import com.android.net.module.util.netlink.InetDiagMessage;
-import com.android.net.module.util.netlink.NetlinkMessage;
-import com.android.net.module.util.netlink.StructInetDiagSockId;
-
-import java.io.PrintWriter;
-
-/**
- * Monitor socket destroy and delete entry from cookie tag bpf map.
- */
-public class SkDestroyListener extends NetlinkMonitor {
-    private static final int SKNLGRP_INET_TCP_DESTROY = 1;
-    private static final int SKNLGRP_INET_UDP_DESTROY = 2;
-    private static final int SKNLGRP_INET6_TCP_DESTROY = 3;
-    private static final int SKNLGRP_INET6_UDP_DESTROY = 4;
-
-    // TODO: if too many sockets are closed too quickly, this can overflow the socket buffer, and
-    // some entries in mCookieTagMap will not be freed. In order to fix this it would be needed to
-    // periodically dump all sockets and remove the tag entries for sockets that have been closed.
-    // For now, set a large-enough buffer that hundreds of sockets can be closed without getting
-    // ENOBUFS and leaking mCookieTagMap entries.
-    private static final int SOCK_RCV_BUF_SIZE = 512 * 1024;
-
-    private final IBpfMap<CookieTagMapKey, CookieTagMapValue> mCookieTagMap;
-
-    SkDestroyListener(final IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap,
-            final Handler handler, final SharedLog log) {
-        super(handler, log, "SkDestroyListener", NETLINK_INET_DIAG,
-                1 << (SKNLGRP_INET_TCP_DESTROY - 1)
-                        | 1 << (SKNLGRP_INET_UDP_DESTROY - 1)
-                        | 1 << (SKNLGRP_INET6_TCP_DESTROY - 1)
-                        | 1 << (SKNLGRP_INET6_UDP_DESTROY - 1),
-                SOCK_RCV_BUF_SIZE);
-        mCookieTagMap = cookieTagMap;
-    }
-
-    @Override
-    public void processNetlinkMessage(final NetlinkMessage nlMsg, final long whenMs) {
-        if (!(nlMsg instanceof InetDiagMessage)) {
-            mLog.e("Received non InetDiagMessage");
-            return;
-        }
-        final StructInetDiagSockId sockId = ((InetDiagMessage) nlMsg).inetDiagMsg.id;
-        try {
-            mCookieTagMap.deleteEntry(new CookieTagMapKey(sockId.cookie));
-        } catch (ErrnoException e) {
-            mLog.e("Failed to delete CookieTagMap entry for " + sockId.cookie  + ": " + e);
-        }
-    }
-
-    /**
-     * Dump the contents of SkDestroyListener log.
-     */
-    public void dump(PrintWriter pw) {
-        mLog.reverseDump(pw);
-    }
-}
diff --git a/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
index 667aad1..4f99d1b 100644
--- a/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
+++ b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
@@ -29,7 +29,10 @@
 /**
  * A thread-safe cache for storing and retrieving {@link NetworkStats.Entry} objects,
  * with an adjustable expiry duration to manage data freshness.
+ *
+ * @deprecated Use {@link LruCacheWithExpiry} instead.
  */
+// TODO: Remove this when service side rate limit cache solution is removed.
 class TrafficStatsRateLimitCache extends
         LruCacheWithExpiry<TrafficStatsRateLimitCache.TrafficStatsCacheKey, NetworkStats.Entry> {
 
@@ -41,7 +44,7 @@
      * @param maxSize Maximum number of entries.
      */
     TrafficStatsRateLimitCache(@NonNull Clock clock, long expiryDurationMs, int maxSize) {
-        super(clock, expiryDurationMs, maxSize, it -> !it.isEmpty());
+        super(()-> clock.millis(), expiryDurationMs, maxSize, it -> !it.isEmpty());
     }
 
     public static class TrafficStatsCacheKey {
diff --git a/service/Android.bp b/service/Android.bp
index fd3d4a3..8b469e4 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -113,7 +113,6 @@
         ":services.connectivity-netstats-jni-sources",
         "jni/com_android_server_connectivity_ClatCoordinator.cpp",
         "jni/com_android_server_ServiceManagerWrapper.cpp",
-        "jni/com_android_server_TestNetworkService.cpp",
         "jni/onload.cpp",
     ],
     header_libs: [
@@ -125,7 +124,7 @@
         "libmodules-utils-build",
         "libnetjniutils",
         "libnet_utils_device_common_bpfjni",
-        "libnet_utils_device_common_timerfdjni",
+        "libserviceconnectivityjni",
         "netd_aidl_interface-lateststable-ndk",
     ],
     shared_libs: [
@@ -162,6 +161,7 @@
     ],
     libs: [
         "framework-annotations-lib",
+        "framework-bluetooth.stubs.module_lib",
         "framework-configinfrastructure.stubs.module_lib",
         "framework-connectivity-pre-jarjar",
         // The framework-connectivity-t library is only available on T+ platforms
@@ -207,6 +207,7 @@
     },
     visibility: [
         "//packages/modules/Connectivity/service-t",
+        "//packages/modules/Connectivity/service-b",
         "//packages/modules/Connectivity/networksecurity:__subpackages__",
         "//packages/modules/Connectivity/tests:__subpackages__",
         "//packages/modules/Connectivity/thread/service:__subpackages__",
@@ -252,7 +253,11 @@
         "service-networksecurity-pre-jarjar",
         service_remoteauth_pre_jarjar_lib,
         "service-thread-pre-jarjar",
-    ],
+    ] + select(release_flag("RELEASE_MOVE_VCN_TO_MAINLINE"), {
+        true: ["service-connectivity-b-pre-jarjar"],
+        default: [],
+    }),
+
     // The below libraries are not actually needed to build since no source is compiled
     // (only combining prebuilt static_libs), but they are necessary so that R8 has the right
     // references to optimize the code. Without these, there will be missing class warnings and
@@ -338,6 +343,7 @@
     name: "service-connectivity-jarjar-gen",
     tool_files: [
         ":service-connectivity-pre-jarjar{.jar}",
+        ":service-connectivity-b-pre-jarjar{.jar}",
         ":service-connectivity-tiramisu-pre-jarjar{.jar}",
         "jarjar-excludes.txt",
     ],
@@ -347,6 +353,7 @@
     out: ["service_connectivity_jarjar_rules.txt"],
     cmd: "$(location jarjar-rules-generator) " +
         "$(location :service-connectivity-pre-jarjar{.jar}) " +
+        "$(location :service-connectivity-b-pre-jarjar{.jar}) " +
         "$(location :service-connectivity-tiramisu-pre-jarjar{.jar}) " +
         "--prefix android.net.connectivity " +
         "--excludes $(location jarjar-excludes.txt) " +
diff --git a/service/ServiceConnectivityResources/OWNERS b/service/ServiceConnectivityResources/OWNERS
index df41ff2..c3c08ee 100644
--- a/service/ServiceConnectivityResources/OWNERS
+++ b/service/ServiceConnectivityResources/OWNERS
@@ -1,2 +1,3 @@
+per-file res/raw/ct_public_keys.pem = file:platform/packages/modules/Connectivity:main:/networksecurity/OWNERS
 per-file res/values/config_thread.xml = file:platform/packages/modules/Connectivity:main:/thread/OWNERS
 per-file res/values/overlayable.xml = file:platform/packages/modules/Connectivity:main:/thread/OWNERS
diff --git a/service/ServiceConnectivityResources/res/raw/ct_public_keys.pem b/service/ServiceConnectivityResources/res/raw/ct_public_keys.pem
new file mode 100644
index 0000000..80dccbe
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/raw/ct_public_keys.pem
@@ -0,0 +1,42 @@
+-----BEGIN PUBLIC KEY-----
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnmb1lacOnP5H1bwb06mG
+fEUeC9PZRwNQskSs9KaWrpfrSkLKuHXkVCbgeagbUR/Sh1OeIhyJRSS0PLCO0JjC
+UpGhYMrIGRgEET4IrP9f8aMFqxxxBUEanI+OxAhIJlP9tiWfGdKAASYcxg/DyXXz
+bqSXBEFJqBLqmnfHcLB/NhO1ejV6AiU1NMrT+iWSrJG8b6mq/LlAqWvidK8oPBsW
+87M4pPLpUoA54ultjx2wEzJ9dBy6jtnKZ/dz4DkDhYug5izRDcYtEfzQBoum0etV
+s4EoogW1AMeqW5G+L4HjPNgp3gNGZ/2RaBy7gp8Br+byYu2wHwdQIBQjS310yaKc
+nuNFOd+Q0DrzvHKB7yYzzdwo+hNocPpkvOzSw74jd09kDZQ+S2peCJ5NPU7VKT6/
+tkvc3tYA9pAu4/T+BGqRft4FjgeNANfSIX/WhWDzzVWymTUGFUvt+D0fF3Cw+XSa
+b8uTgRZ1Ma/FvSGgXHVoG9E4QAFFG4I9mmRqsnzA+8fqSNAfieL5OWecq4PU+pMa
+uNVJ9hbvmW2yXuMgEg6K9kFLdxggRn+OcxowgXJJh79L0r7RN1d8kuHelDhOzcte
+dUTtLNOb/1PA0d/I2IVJwc9xSQZXurqqT/Z+c01B3/R0BgGDkIT/yZ5iHPoZFYPD
+U8UdQzUK0KXgGkc8P5pJW8kCAwEAAQ==
+-----END PUBLIC KEY-----
+-----BEGIN PUBLIC KEY-----
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAn1ssK1m56VVurVJ/fNKe
++aXDnytBy/hWY48ZT+ZC0S29llfjkaCBlmkngoI2hgwz9BI6pHwUINS15pT1sznw
+kHoEaNKr8sANHTQ0PYlDuk5iQSjnaGz7XmqbZ3c92BQrmLG/kwX2c17YNJI2qCk3
+MaBJBeS5YErR3xcucT9M7qtNWWIT9O0sLV1lDUZVCYedWBSnNz1/mAiLhttWmU5u
+GKl/5LmjWP/piNjh8whx0abJUGeGS2HH0JAOb0pjBV6UQvj3tO+gTiNDhdrE4CKh
+Qn8SKNjW/BY320f0A5t581Q0++cQUAisRgBQos0Pkvg5vb6wgII+pJq0SnZoYFfH
+oycuR4q3eOCgJmpEAPC0MhNpIDUQS6p3QabD9ID+21ymiQa/Zf8Mv2xMM6ZItKxX
+77vSKvBbimTGmB2IU+Zi484PKI5QwxBUCHVSmNpvHyXyjhBmpqik9Op26QYYT10b
+ADnJY1L/Q+i44nI4pfwgIncqAWuLnxg/XggJDWj48Un9SMNoyN7gzQX75M7rh7/t
+F6QtKvJreP0pP2UoVSgZKjXnL9tqeZbOdZU1kBHi1HOhlUKTfq5dn2fVUeYkE769
+clFF7Y1FiI259IPhTKiOIfARJ4BL4Sn8D9c9vpxDYPFl5bCJbspmFpwfzTMDnGVS
+/IlY6Putpv2/lD1B7aQGt1sCAwEAAQ==
+-----END PUBLIC KEY-----
+-----BEGIN PUBLIC KEY-----
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwMPNecDhLDamK9Rs8W5V
+c0LWZPbk9FNP+budBoFMX46dWyJ75O5e8xY9UhJSBFl8+Itm/rWO/h2zZ6qE3Cq8
+1LYbOg+x+rLnYOcAvD2O7EU73A3RD/vqoUDDVK3cwKMq3ry7CYu+NJW7TRIKV7Ct
+BMCBrvpmC1WlZ/jxSV0Soapza4/H+UV0hHYh/Wn3EWObGWYdI3yxwZ81AyU1QCR/
+oaO0PQPXqvo3gPBINnK3Qr0aLtYc4YCfTXe6i4g3DeAlkpqNLZtC2hyqiRB4Dg47
+zDzYECGofRAu8w9d8uI+eccacXfvI9zEcL7FAl5AzBKmMFOfBNTr08V7+aROWfGO
+7imWsj2MQ6RG17zqJak5QX/1bqDxwhG0KolB86mPZu0WeKz1B3iP5qAUlDNBHLDV
+pQIez0mrMsVsimVguuLYHMpIgijphA9WhijCJW2x7c6aocB6IpnMIV1sqnUQTwLG
+t32AMrckxqFmaKGj/8I9M+Xj+Cy4fIa5YSOdb/tlaYZSfjH5ch41xucQ2HWFyZ/9
+hkTFodvF5ajCQ5maHeIjDkS/Bc/s9CB+/fbSkstDsPMRp/ExyQcEYjKTG5o9Ewyo
++KGGXS2dSS10Ibl0Zx/S/0ZuZx8ZAxMOIIPpugdkWqHU9thh71dR8zM4KMkEfB8C
+sWLGB1yMuztn9nRUcpiEZTECAwEAAQ==
+-----END PUBLIC KEY-----
diff --git a/service/ServiceConnectivityResources/res/values/config_thread.xml b/service/ServiceConnectivityResources/res/values/config_thread.xml
index 4027038..d1d9e52 100644
--- a/service/ServiceConnectivityResources/res/values/config_thread.xml
+++ b/service/ServiceConnectivityResources/res/values/config_thread.xml
@@ -71,4 +71,18 @@
     -->
     <string-array name="config_thread_mdns_vendor_specific_txts">
     </string-array>
+
+    <!-- Whether to enable / start SRP server only when border routing is ready. SRP server and
+    border routing are mandatory features required by a Thread Border Router, and it takes 10 to
+    20 seconds to establish border routing. Starting SRP server earlier is useful for use cases
+    where the user needs to know what are the devices in the network before actually needs to reach
+    to the devices, or reaching to Thread end devices doesn't require border routing to work.
+    -->
+    <bool name="config_thread_srp_server_wait_for_border_routing_enabled">true</bool>
+
+    <!-- Whether this border router will automatically join the previous connected network after
+    device reboots. Setting this value to false can allow the user to control the lifecycle of
+    the Thread border router state on this device.
+    -->
+    <bool name="config_thread_border_router_auto_join_enabled">true</bool>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index fbaae05..7ac86aa 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -52,6 +52,8 @@
             <item type="string" name="config_thread_vendor_oui" />
             <item type="string" name="config_thread_model_name" />
             <item type="array" name="config_thread_mdns_vendor_specific_txts" />
+            <item type="bool" name="config_thread_srp_server_wait_for_border_routing_enabled" />
+            <item type="bool" name="config_thread_border_router_auto_join_enabled" />
         </policy>
     </overlayable>
 </resources>
diff --git a/service/jarjar-excludes.txt b/service/jarjar-excludes.txt
index 7bd3862..9076b53 100644
--- a/service/jarjar-excludes.txt
+++ b/service/jarjar-excludes.txt
@@ -1,3 +1,4 @@
 # Classes loaded by SystemServer via their hardcoded name, so they can't be jarjared
 com\.android\.server\.ConnectivityServiceInitializer(\$.+)?
+com\.android\.server\.ConnectivityServiceInitializerB(\$.+)?
 com\.android\.server\.NetworkStatsServiceInitializer(\$.+)?
diff --git a/service/jni/com_android_server_TestNetworkService.cpp b/service/jni/com_android_server_TestNetworkService.cpp
deleted file mode 100644
index 08d31a3..0000000
--- a/service/jni/com_android_server_TestNetworkService.cpp
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#define LOG_NDEBUG 0
-
-#define LOG_TAG "TestNetworkServiceJni"
-
-#include <arpa/inet.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <linux/if.h>
-#include <linux/if_tun.h>
-#include <linux/ipv6_route.h>
-#include <linux/route.h>
-#include <netinet/in.h>
-#include <stdio.h>
-#include <string.h>
-#include <sys/ioctl.h>
-#include <sys/socket.h>
-#include <sys/stat.h>
-#include <sys/types.h>
-
-#include <log/log.h>
-
-#include "jni.h"
-#include <android-base/stringprintf.h>
-#include <android-base/unique_fd.h>
-#include <bpf/KernelUtils.h>
-#include <nativehelper/JNIHelp.h>
-#include <nativehelper/ScopedUtfChars.h>
-
-#ifndef IFF_NO_CARRIER
-#define IFF_NO_CARRIER 0x0040
-#endif
-
-namespace android {
-
-//------------------------------------------------------------------------------
-
-static void throwException(JNIEnv* env, int error, const char* action, const char* iface) {
-    const std::string& msg = "Error: " + std::string(action) + " " + std::string(iface) +  ": "
-                + std::string(strerror(error));
-    jniThrowException(env, "java/lang/IllegalStateException", msg.c_str());
-}
-
-// enable or disable  carrier on tun / tap interface.
-static void setTunTapCarrierEnabledImpl(JNIEnv* env, const char* iface, int tunFd, bool enabled) {
-    uint32_t carrierOn = enabled;
-    if (ioctl(tunFd, TUNSETCARRIER, &carrierOn)) {
-        throwException(env, errno, "set carrier", iface);
-    }
-}
-
-static int createTunTapImpl(JNIEnv* env, bool isTun, bool hasCarrier, bool setIffMulticast,
-                            const char* iface) {
-    base::unique_fd tun(open("/dev/tun", O_RDWR | O_NONBLOCK));
-    ifreq ifr{};
-
-    // Allocate interface.
-    ifr.ifr_flags = (isTun ? IFF_TUN : IFF_TAP) | IFF_NO_PI;
-    if (!hasCarrier) {
-        // Using IFF_NO_CARRIER is supported starting in kernel version >= 6.0
-        // Up until then, unsupported flags are ignored.
-        if (!bpf::isAtLeastKernelVersion(6, 0, 0)) {
-            throwException(env, EOPNOTSUPP, "IFF_NO_CARRIER not supported", ifr.ifr_name);
-            return -1;
-        }
-        ifr.ifr_flags |= IFF_NO_CARRIER;
-    }
-    strlcpy(ifr.ifr_name, iface, IFNAMSIZ);
-    if (ioctl(tun.get(), TUNSETIFF, &ifr)) {
-        throwException(env, errno, "allocating", ifr.ifr_name);
-        return -1;
-    }
-
-    // Mark some TAP interfaces as supporting multicast
-    if (setIffMulticast && !isTun) {
-        base::unique_fd inet6CtrlSock(socket(AF_INET6, SOCK_DGRAM, 0));
-        ifr.ifr_flags = IFF_MULTICAST;
-
-        if (ioctl(inet6CtrlSock.get(), SIOCSIFFLAGS, &ifr)) {
-            throwException(env, errno, "set IFF_MULTICAST", ifr.ifr_name);
-            return -1;
-        }
-    }
-
-    return tun.release();
-}
-
-static void bringUpInterfaceImpl(JNIEnv* env, const char* iface) {
-    // Activate interface using an unconnected datagram socket.
-    base::unique_fd inet6CtrlSock(socket(AF_INET6, SOCK_DGRAM, 0));
-
-    ifreq ifr{};
-    strlcpy(ifr.ifr_name, iface, IFNAMSIZ);
-    if (ioctl(inet6CtrlSock.get(), SIOCGIFFLAGS, &ifr)) {
-        throwException(env, errno, "read flags", iface);
-        return;
-    }
-    ifr.ifr_flags |= IFF_UP;
-    if (ioctl(inet6CtrlSock.get(), SIOCSIFFLAGS, &ifr)) {
-        throwException(env, errno, "set IFF_UP", iface);
-        return;
-    }
-}
-
-//------------------------------------------------------------------------------
-
-
-
-static void setTunTapCarrierEnabled(JNIEnv* env, jclass /* clazz */, jstring
-                                    jIface, jint tunFd, jboolean enabled) {
-    ScopedUtfChars iface(env, jIface);
-    if (!iface.c_str()) {
-        jniThrowNullPointerException(env, "iface");
-        return;
-    }
-    setTunTapCarrierEnabledImpl(env, iface.c_str(), tunFd, enabled);
-}
-
-static jint createTunTap(JNIEnv* env, jclass /* clazz */, jboolean isTun,
-                             jboolean hasCarrier, jboolean setIffMulticast, jstring jIface) {
-    ScopedUtfChars iface(env, jIface);
-    if (!iface.c_str()) {
-        jniThrowNullPointerException(env, "iface");
-        return -1;
-    }
-
-    return createTunTapImpl(env, isTun, hasCarrier, setIffMulticast, iface.c_str());
-}
-
-static void bringUpInterface(JNIEnv* env, jclass /* clazz */, jstring jIface) {
-    ScopedUtfChars iface(env, jIface);
-    if (!iface.c_str()) {
-        jniThrowNullPointerException(env, "iface");
-        return;
-    }
-    bringUpInterfaceImpl(env, iface.c_str());
-}
-
-//------------------------------------------------------------------------------
-
-static const JNINativeMethod gMethods[] = {
-    {"nativeSetTunTapCarrierEnabled", "(Ljava/lang/String;IZ)V", (void*)setTunTapCarrierEnabled},
-    {"nativeCreateTunTap", "(ZZZLjava/lang/String;)I", (void*)createTunTap},
-    {"nativeBringUpInterface", "(Ljava/lang/String;)V", (void*)bringUpInterface},
-};
-
-int register_com_android_server_TestNetworkService(JNIEnv* env) {
-    return jniRegisterNativeMethods(env,
-            "android/net/connectivity/com/android/server/TestNetworkService", gMethods,
-            NELEM(gMethods));
-}
-
-}; // namespace android
diff --git a/service/jni/onload.cpp b/service/jni/onload.cpp
index 8e01260..f87470d 100644
--- a/service/jni/onload.cpp
+++ b/service/jni/onload.cpp
@@ -21,12 +21,11 @@
 
 namespace android {
 
-int register_com_android_server_TestNetworkService(JNIEnv* env);
 int register_com_android_server_connectivity_ClatCoordinator(JNIEnv* env);
 int register_android_server_net_NetworkStatsFactory(JNIEnv* env);
 int register_android_server_net_NetworkStatsService(JNIEnv* env);
 int register_com_android_server_ServiceManagerWrapper(JNIEnv* env);
-int register_com_android_net_module_util_TimerFdUtils(JNIEnv *env,
+int register_com_android_net_module_util_ServiceConnectivityJni(JNIEnv *env,
                                                       char const *class_name);
 
 extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
@@ -36,10 +35,6 @@
         return JNI_ERR;
     }
 
-    if (register_com_android_server_TestNetworkService(env) < 0) {
-        return JNI_ERR;
-    }
-
     if (register_com_android_server_ServiceManagerWrapper(env) < 0) {
         return JNI_ERR;
     }
@@ -58,9 +53,9 @@
         }
     }
 
-    if (register_com_android_net_module_util_TimerFdUtils(
+    if (register_com_android_net_module_util_ServiceConnectivityJni(
             env, "android/net/connectivity/com/android/net/module/util/"
-                 "TimerFdUtils") < 0) {
+                 "ServiceConnectivityJni") < 0) {
       return JNI_ERR;
     }
 
diff --git a/service/libconnectivity/include/connectivity_native.h b/service/libconnectivity/include/connectivity_native.h
index f4676a9..f264b68 100644
--- a/service/libconnectivity/include/connectivity_native.h
+++ b/service/libconnectivity/include/connectivity_native.h
@@ -20,12 +20,6 @@
 #include <sys/cdefs.h>
 #include <netinet/in.h>
 
-// For branches that do not yet have __ANDROID_API_U__ defined, like module
-// release branches.
-#ifndef __ANDROID_API_U__
-#define __ANDROID_API_U__ 34
-#endif
-
 __BEGIN_DECLS
 
 /**
@@ -41,7 +35,7 @@
  *
  * @param port Int corresponding to port number.
  */
-int AConnectivityNative_blockPortForBind(in_port_t port) __INTRODUCED_IN(__ANDROID_API_U__);
+int AConnectivityNative_blockPortForBind(in_port_t port) __INTRODUCED_IN(34);
 
 /**
  * Unblocks a port that has previously been blocked.
@@ -54,7 +48,7 @@
  *
  * @param port Int corresponding to port number.
  */
-int AConnectivityNative_unblockPortForBind(in_port_t port) __INTRODUCED_IN(__ANDROID_API_U__);
+int AConnectivityNative_unblockPortForBind(in_port_t port) __INTRODUCED_IN(34);
 
 /**
  * Unblocks all ports that have previously been blocked.
@@ -64,7 +58,7 @@
  *  - EPERM if the UID of the client doesn't have network stack permission
  *  - Other errors as per https://man7.org/linux/man-pages/man2/bpf.2.html
  */
-int AConnectivityNative_unblockAllPortsForBind() __INTRODUCED_IN(__ANDROID_API_U__);
+int AConnectivityNative_unblockAllPortsForBind() __INTRODUCED_IN(34);
 
 /**
  * Gets the list of ports that have been blocked.
@@ -79,7 +73,7 @@
  *              blocked ports, which may be larger than the ports array that was filled.
  */
 int AConnectivityNative_getPortsBlockedForBind(in_port_t* _Nonnull ports, size_t* _Nonnull count)
-    __INTRODUCED_IN(__ANDROID_API_U__);
+    __INTRODUCED_IN(34);
 
 __END_DECLS
 
diff --git a/service/native/libs/libclat/Android.bp b/service/native/libs/libclat/Android.bp
index 6c1c2c4..9554bd8 100644
--- a/service/native/libs/libclat/Android.bp
+++ b/service/native/libs/libclat/Android.bp
@@ -47,6 +47,7 @@
     srcs: [
         "clatutils_test.cpp",
     ],
+    stl: "libc++_static",
     header_libs: [
         "bpf_connectivity_headers",
     ],
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index 44868b2d..36c0cf9 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -25,6 +25,8 @@
 import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_MAP_PATH;
 import static android.net.BpfNetMapsConstants.IIF_MATCH;
 import static android.net.BpfNetMapsConstants.INGRESS_DISCARD_MAP_PATH;
+import static android.net.BpfNetMapsConstants.LOCAL_NET_ACCESS_MAP_PATH;
+import static android.net.BpfNetMapsConstants.LOCAL_NET_BLOCKED_UID_MAP_PATH;
 import static android.net.BpfNetMapsConstants.LOCKDOWN_VPN_MATCH;
 import static android.net.BpfNetMapsConstants.UID_OWNER_MAP_PATH;
 import static android.net.BpfNetMapsConstants.UID_PERMISSION_MAP_PATH;
@@ -36,16 +38,16 @@
 import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
 import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
 import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
-import static android.net.INetd.PERMISSION_INTERNET;
-import static android.net.INetd.PERMISSION_NONE;
-import static android.net.INetd.PERMISSION_UNINSTALLED;
-import static android.net.INetd.PERMISSION_UPDATE_DEVICE_STATS;
 import static android.system.OsConstants.EINVAL;
 import static android.system.OsConstants.ENODEV;
 import static android.system.OsConstants.ENOENT;
 import static android.system.OsConstants.EOPNOTSUPP;
 
 import static com.android.server.ConnectivityStatsLog.NETWORK_BPF_MAP_INFO;
+import static com.android.server.connectivity.NetworkPermissions.PERMISSION_NONE;
+import static com.android.server.connectivity.NetworkPermissions.TRAFFIC_PERMISSION_INTERNET;
+import static com.android.server.connectivity.NetworkPermissions.TRAFFIC_PERMISSION_UNINSTALLED;
+import static com.android.server.connectivity.NetworkPermissions.TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS;
 
 import android.app.StatsManager;
 import android.content.Context;
@@ -74,6 +76,7 @@
 import com.android.net.module.util.IBpfMap;
 import com.android.net.module.util.SingleWriterBpfMap;
 import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Bool;
 import com.android.net.module.util.Struct.S32;
 import com.android.net.module.util.Struct.U32;
 import com.android.net.module.util.Struct.U8;
@@ -81,6 +84,7 @@
 import com.android.net.module.util.bpf.CookieTagMapValue;
 import com.android.net.module.util.bpf.IngressDiscardKey;
 import com.android.net.module.util.bpf.IngressDiscardValue;
+import com.android.net.module.util.bpf.LocalNetAccessKey;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
@@ -131,9 +135,12 @@
     private static IBpfMap<S32, U8> sDataSaverEnabledMap = null;
     private static IBpfMap<IngressDiscardKey, IngressDiscardValue> sIngressDiscardMap = null;
 
+    private static IBpfMap<LocalNetAccessKey, Bool> sLocalNetAccessMap = null;
+    private static IBpfMap<U32, Bool> sLocalNetBlockedUidMap = null;
+
     private static final List<Pair<Integer, String>> PERMISSION_LIST = Arrays.asList(
-            Pair.create(PERMISSION_INTERNET, "PERMISSION_INTERNET"),
-            Pair.create(PERMISSION_UPDATE_DEVICE_STATS, "PERMISSION_UPDATE_DEVICE_STATS")
+            Pair.create(TRAFFIC_PERMISSION_INTERNET, "PERMISSION_INTERNET"),
+            Pair.create(TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS, "PERMISSION_UPDATE_DEVICE_STATS")
     );
 
     /**
@@ -186,6 +193,25 @@
         sIngressDiscardMap = ingressDiscardMap;
     }
 
+    /**
+     * Set localNetAccessMap for test.
+     */
+    @VisibleForTesting
+    public static void setLocalNetAccessMapForTest(
+            IBpfMap<LocalNetAccessKey, Bool> localNetAccessMap) {
+        sLocalNetAccessMap = localNetAccessMap;
+    }
+
+    /**
+     * Set localNetBlockedUidMap for test.
+     */
+    @VisibleForTesting
+    public static void setLocalNetBlockedUidMapForTest(
+            IBpfMap<U32, Bool> localNetBlockedUidMap) {
+        sLocalNetBlockedUidMap = localNetBlockedUidMap;
+    }
+
+
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private static IBpfMap<S32, U32> getConfigurationMap() {
         try {
@@ -247,6 +273,26 @@
         }
     }
 
+    @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+    private static IBpfMap<U32, Bool> getLocalNetBlockedUidMap() {
+        try {
+            return SingleWriterBpfMap.getSingleton(LOCAL_NET_BLOCKED_UID_MAP_PATH,
+                    U32.class, Bool.class);
+        } catch (ErrnoException e) {
+            throw new IllegalStateException("Cannot open local_net_blocked_uid map", e);
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+    private static IBpfMap<LocalNetAccessKey, Bool> getLocalNetAccessMap() {
+        try {
+            return SingleWriterBpfMap.getSingleton(LOCAL_NET_ACCESS_MAP_PATH,
+                    LocalNetAccessKey.class, Bool.class);
+        } catch (ErrnoException e) {
+            throw new IllegalStateException("Cannot open local_net_access map", e);
+        }
+    }
+
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private static void initBpfMaps() {
         if (sConfigurationMap == null) {
@@ -299,6 +345,27 @@
         } catch (ErrnoException e) {
             throw new IllegalStateException("Failed to initialize ingress discard map", e);
         }
+
+        if (isAtLeast25Q2()) {
+            if (sLocalNetAccessMap == null) {
+                sLocalNetAccessMap = getLocalNetAccessMap();
+            }
+            try {
+                sLocalNetAccessMap.clear();
+            } catch (ErrnoException e) {
+                throw new IllegalStateException("Failed to initialize local_net_access map", e);
+            }
+
+            if (sLocalNetBlockedUidMap == null) {
+                sLocalNetBlockedUidMap = getLocalNetBlockedUidMap();
+            }
+            try {
+                sLocalNetBlockedUidMap.clear();
+            } catch (ErrnoException e) {
+                throw new IllegalStateException("Failed to initialize local_net_blocked_uid map",
+                        e);
+            }
+        }
     }
 
     /**
@@ -387,6 +454,21 @@
         }
     }
 
+    private void throwIfPre25Q2(final String msg) {
+        if (!isAtLeast25Q2()) {
+            throw new UnsupportedOperationException(msg);
+        }
+    }
+
+    /*
+     ToDo : Remove this method when SdkLevel.isAtLeastB() is fixed, aosp is at sdk level 36 or use
+     NetworkStackUtils.isAtLeast25Q2 when it is moved to a static lib.
+     */
+    public static boolean isAtLeast25Q2() {
+        return SdkLevel.isAtLeastB()  || (SdkLevel.isAtLeastV()
+                && "Baklava".equals(Build.VERSION.CODENAME));
+    }
+
     private void removeRule(final int uid, final long match, final String caller) {
         try {
             synchronized (sUidOwnerMap) {
@@ -788,7 +870,8 @@
         }
 
         // Remove the entry if package is uninstalled or uid has only INTERNET permission.
-        if (permissions == PERMISSION_UNINSTALLED || permissions == PERMISSION_INTERNET) {
+        if (permissions == TRAFFIC_PERMISSION_UNINSTALLED
+                || permissions == TRAFFIC_PERMISSION_INTERNET) {
             for (final int uid : uids) {
                 try {
                     sUidPermissionMap.deleteEntry(new S32(uid));
@@ -810,6 +893,158 @@
     }
 
     /**
+     * Add configuration to local_net_access trie map.
+     * @param lpmBitlen prefix length that will be used for longest matching
+     * @param iface interface name
+     * @param address remote address. ipv4 addresses would be mapped to v6
+     * @param protocol required for longest match in special cases
+     * @param remotePort src/dst port for ingress/egress
+     * @param isAllowed is the local network call allowed or blocked.
+     */
+    @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+    public void addLocalNetAccess(final int lpmBitlen, final String iface,
+            final InetAddress address, final int protocol, final int remotePort,
+            final boolean isAllowed) {
+        throwIfPre25Q2("addLocalNetAccess is not available on pre-B devices");
+        final int ifIndex;
+        if (iface == null) {
+            ifIndex = 0;
+        } else {
+            ifIndex = mDeps.getIfIndex(iface);
+        }
+        if (ifIndex == 0) {
+            Log.e(TAG, "Failed to get if index, skip addLocalNetAccess for " + address
+                    + "(" + iface + ")");
+            return;
+        }
+        final LocalNetAccessKey localNetAccessKey = new LocalNetAccessKey(lpmBitlen, ifIndex,
+                address, protocol, remotePort);
+
+        try {
+            sLocalNetAccessMap.updateEntry(localNetAccessKey, new Bool(isAllowed));
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Failed to add local network access for localNetAccessKey : "
+                    + localNetAccessKey + ", isAllowed : " + isAllowed);
+        }
+    }
+
+    /**
+     * Remove configuration to local_net_access trie map.
+     * @param lpmBitlen prefix length that will be used for longest matching
+     * @param iface interface name
+     * @param address remote address. ipv4 addresses would be mapped to v6
+     * @param protocol required for longest match in special cases
+     * @param remotePort src/dst port for ingress/egress
+     */
+    @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+    public void removeLocalNetAccess(final int lpmBitlen, final String iface,
+            final InetAddress address, final int protocol, final int remotePort) {
+        throwIfPre25Q2("removeLocalNetAccess is not available on pre-B devices");
+        final int ifIndex;
+        if (iface == null) {
+            ifIndex = 0;
+        } else {
+            ifIndex = mDeps.getIfIndex(iface);
+        }
+        if (ifIndex == 0) {
+            Log.e(TAG, "Failed to get if index, skip removeLocalNetAccess for " + address
+                    + "(" + iface + ")");
+            return;
+        }
+        final LocalNetAccessKey localNetAccessKey = new LocalNetAccessKey(lpmBitlen, ifIndex,
+                address, protocol, remotePort);
+
+        try {
+            sLocalNetAccessMap.deleteEntry(localNetAccessKey);
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Failed to remove local network access for localNetAccessKey : "
+                    + localNetAccessKey);
+        }
+    }
+
+    /**
+     * Fetches value available in local_net_access bpf map for provided configuration
+     * @param lpmBitlen  prefix length that will be used for longest matching
+     * @param iface    interface name
+     * @param address    remote address. ipv4 addresses would be mapped to v6
+     * @param protocol   required for longest match in special cases
+     * @param remotePort src/dst port for ingress/egress
+     * @return false if the configuration is disallowed, true if the configuration is absent i.e. it
+     * is not local network or if configuration is allowed like local dns servers.
+     */
+    @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+    public boolean getLocalNetAccess(final int lpmBitlen, final String iface,
+            final InetAddress address, final int protocol, final int remotePort) {
+        throwIfPre25Q2("getLocalNetAccess is not available on pre-B devices");
+        final int ifIndex;
+        if (iface == null) {
+            ifIndex = 0;
+        } else {
+            ifIndex = mDeps.getIfIndex(iface);
+        }
+        if (ifIndex == 0) {
+            Log.e(TAG, "Failed to get if index, returning default from getLocalNetAccess for "
+                    + address + "(" + iface + ")");
+            return true;
+        }
+        final LocalNetAccessKey localNetAccessKey = new LocalNetAccessKey(lpmBitlen, ifIndex,
+                address, protocol, remotePort);
+        try {
+            final Bool value = sLocalNetAccessMap.getValue(localNetAccessKey);
+            return value == null ? true : value.val;
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Failed to find local network access configuration for "
+                    + "localNetAccessKey : " + localNetAccessKey);
+        }
+        return true;
+    }
+
+    /**
+     * Add uid to local_net_blocked_uid map.
+     * @param uid application uid that needs to block local network calls.
+     */
+    @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+    public void addUidToLocalNetBlockMap(final int uid) {
+        throwIfPre25Q2("addUidToLocalNetBlockMap is not available on pre-B devices");
+        try {
+            sLocalNetBlockedUidMap.updateEntry(new U32(uid), new Bool(true));
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Failed to add local network blocked for uid : " + uid);
+        }
+    }
+
+    /**
+     * True if local network calls are blocked for application.
+     * @param uid application uid that needs check if local network calls are blocked.
+     */
+    @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+    public boolean isUidBlockedFromUsingLocalNetwork(final int uid) {
+        throwIfPre25Q2("isUidBlockedFromUsingLocalNetwork is not available on pre-B devices");
+        try {
+            final Bool value = sLocalNetBlockedUidMap.getValue(new U32(uid));
+            return value == null ? false : value.val;
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Failed to find uid(" + uid
+                    + ") is present in local network blocked map");
+        }
+        return false;
+    }
+
+    /**
+     * Remove uid from local_net_blocked_uid map(if present).
+     * @param uid application uid that needs check if local network calls are blocked.
+     */
+    @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+    public void removeUidFromLocalNetBlockMap(final int uid) {
+        throwIfPre25Q2("removeUidFromLocalNetBlockMap is not available on pre-B devices");
+        try {
+            sLocalNetBlockedUidMap.deleteEntry(new U32(uid));
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Failed to remove uid(" + uid + ") from local network blocked map");
+        }
+    }
+
+    /**
      * Get granted permissions for specified uid. If uid is not in the map, this method returns
      * {@link android.net.INetd.PERMISSION_INTERNET} since this is a default permission.
      * See {@link #setNetPermForUids}
@@ -824,10 +1059,10 @@
             // Key of uid permission map is appId
             // TODO: Rename map name
             final U8 permissions = sUidPermissionMap.getValue(new S32(appId));
-            return permissions != null ? permissions.val : PERMISSION_INTERNET;
+            return permissions != null ? permissions.val : TRAFFIC_PERMISSION_INTERNET;
         } catch (ErrnoException e) {
             Log.wtf(TAG, "Failed to get permission for uid: " + uid);
-            return PERMISSION_INTERNET;
+            return TRAFFIC_PERMISSION_INTERNET;
         }
     }
 
@@ -976,7 +1211,7 @@
         if (permissionMask == PERMISSION_NONE) {
             return "PERMISSION_NONE";
         }
-        if (permissionMask == PERMISSION_UNINSTALLED) {
+        if (permissionMask == TRAFFIC_PERMISSION_UNINSTALLED) {
             // PERMISSION_UNINSTALLED should never appear in the map
             return "PERMISSION_UNINSTALLED error!";
         }
@@ -1079,6 +1314,14 @@
                     (key, value) -> "[" + key.dstAddr + "]: "
                             + value.iif1 + "(" + mDeps.getIfName(value.iif1) + "), "
                             + value.iif2 + "(" + mDeps.getIfName(value.iif2) + ")");
+            if (sLocalNetBlockedUidMap != null) {
+                BpfDump.dumpMap(sLocalNetAccessMap, pw, "sLocalNetAccessMap",
+                        (key, value) -> "[" + key + "]: " + value);
+            }
+            if (sLocalNetBlockedUidMap != null) {
+                BpfDump.dumpMap(sLocalNetBlockedUidMap, pw, "sLocalNetBlockedUidMap",
+                        (key, value) -> "[" + key + "]: " + value);
+            }
             dumpDataSaverConfig(pw);
             pw.decreaseIndent();
         }
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 0d0f6fc..adf593e 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -48,6 +48,7 @@
 import static android.net.ConnectivityManager.CALLBACK_LOSING;
 import static android.net.ConnectivityManager.CALLBACK_LOST;
 import static android.net.ConnectivityManager.CALLBACK_PRECHECK;
+import static android.net.ConnectivityManager.CALLBACK_RESERVED;
 import static android.net.ConnectivityManager.CALLBACK_RESUMED;
 import static android.net.ConnectivityManager.CALLBACK_SUSPENDED;
 import static android.net.ConnectivityManager.CALLBACK_UNAVAIL;
@@ -108,6 +109,8 @@
 import static android.net.NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION;
 import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS;
 import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS;
+import static android.net.NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS;
+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_VPN;
@@ -121,7 +124,9 @@
 import static android.net.connectivity.ConnectivityCompatChanges.NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION;
 import static android.os.Process.INVALID_UID;
 import static android.os.Process.VPN_UID;
-import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
+import static android.system.OsConstants.ENOENT;
+import static android.system.OsConstants.ENOTCONN;
+import static android.system.OsConstants.EOPNOTSUPP;
 import static android.system.OsConstants.ETH_P_ALL;
 import static android.system.OsConstants.IPPROTO_TCP;
 import static android.system.OsConstants.IPPROTO_UDP;
@@ -141,6 +146,8 @@
 import static com.android.net.module.util.BpfUtils.BPF_CGROUP_UDP6_RECVMSG;
 import static com.android.net.module.util.BpfUtils.BPF_CGROUP_UDP6_SENDMSG;
 import static com.android.net.module.util.NetworkMonitorUtils.isPrivateDnsValidationRequired;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_LOCAL_PREFIXES;
+import static com.android.net.module.util.NetworkStackConstants.MULTICAST_AND_BROADCAST_PREFIXES;
 import static com.android.net.module.util.PermissionUtils.enforceAnyPermissionOf;
 import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermission;
 import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermissionOr;
@@ -149,6 +156,7 @@
 import static com.android.server.connectivity.ConnectivityFlags.CELLULAR_DATA_INACTIVITY_TIMEOUT;
 import static com.android.server.connectivity.ConnectivityFlags.DELAY_DESTROY_SOCKETS;
 import static com.android.server.connectivity.ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING;
+import static com.android.server.connectivity.ConnectivityFlags.NAMESPACE_TETHERING_BOOT;
 import static com.android.server.connectivity.ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS;
 import static com.android.server.connectivity.ConnectivityFlags.REQUEST_RESTRICTED_WIFI;
 import static com.android.server.connectivity.ConnectivityFlags.WIFI_DATA_INACTIVITY_TIMEOUT;
@@ -195,6 +203,7 @@
 import android.net.IConnectivityDiagnosticsCallback;
 import android.net.IConnectivityManager;
 import android.net.IDnsResolver;
+import android.net.IIntResultListener;
 import android.net.INetd;
 import android.net.INetworkActivityListener;
 import android.net.INetworkAgent;
@@ -207,6 +216,7 @@
 import android.net.InetAddresses;
 import android.net.IpMemoryStore;
 import android.net.IpPrefix;
+import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.LocalNetworkConfig;
 import android.net.LocalNetworkInfo;
@@ -554,6 +564,7 @@
     // The Context is created for UserHandle.ALL.
     private final Context mUserAllContext;
     private final Dependencies mDeps;
+    private final PermissionMonitor.Dependencies mPermissionMonitorDeps;
     private final ConnectivityFlags mFlags;
     // 0 is full bad, 100 is full good
     private int mDefaultInetConditionPublished = 0;
@@ -1013,6 +1024,8 @@
     private final LingerMonitor mLingerMonitor;
     private final SatelliteAccessController mSatelliteAccessController;
 
+    private final L2capNetworkProvider mL2capNetworkProvider;
+
     // sequence number of NetworkRequests
     private int mNextNetworkRequestId = NetworkRequest.FIRST_REQUEST_ID;
 
@@ -1613,15 +1626,20 @@
                     connectivityServiceInternalHandler);
         }
 
+        /** Creates an L2capNetworkProvider */
+        public L2capNetworkProvider makeL2capNetworkProvider(Context context) {
+            return new L2capNetworkProvider(context);
+        }
+
         /** Returns the data inactivity timeout to be used for cellular networks */
         public int getDefaultCellularDataInactivityTimeout() {
-            return DeviceConfigUtils.getDeviceConfigPropertyInt(NAMESPACE_TETHERING,
+            return DeviceConfigUtils.getDeviceConfigPropertyInt(NAMESPACE_TETHERING_BOOT,
                     CELLULAR_DATA_INACTIVITY_TIMEOUT, 10);
         }
 
         /** Returns the data inactivity timeout to be used for WiFi networks */
         public int getDefaultWifiDataInactivityTimeout() {
-            return DeviceConfigUtils.getDeviceConfigPropertyInt(NAMESPACE_TETHERING,
+            return DeviceConfigUtils.getDeviceConfigPropertyInt(NAMESPACE_TETHERING_BOOT,
                     WIFI_DATA_INACTIVITY_TIMEOUT, 15);
         }
 
@@ -1791,12 +1809,13 @@
     public ConnectivityService(Context context) {
         this(context, getDnsResolver(context), new IpConnectivityLog(),
                 INetd.Stub.asInterface((IBinder) context.getSystemService(Context.NETD_SERVICE)),
-                new Dependencies());
+                new Dependencies(), new PermissionMonitor.Dependencies());
     }
 
     @VisibleForTesting
     protected ConnectivityService(Context context, IDnsResolver dnsresolver,
-            IpConnectivityLog logger, INetd netd, Dependencies deps) {
+            IpConnectivityLog logger, INetd netd, Dependencies deps,
+            PermissionMonitor.Dependencies mPermDeps) {
         if (DBG) log("ConnectivityService starting up");
 
         mDeps = Objects.requireNonNull(deps, "missing Dependencies");
@@ -1869,8 +1888,10 @@
         mNetd = netd;
         mBpfNetMaps = mDeps.getBpfNetMaps(mContext, netd);
         mHandlerThread = mDeps.makeHandlerThread("ConnectivityServiceThread");
+        mPermissionMonitorDeps = mPermDeps;
         mPermissionMonitor =
-                new PermissionMonitor(mContext, mNetd, mBpfNetMaps, mHandlerThread);
+                new PermissionMonitor(mContext, mNetd, mBpfNetMaps, mPermissionMonitorDeps,
+                        mHandlerThread);
         mHandlerThread.start();
         mHandler = new InternalHandler(mHandlerThread.getLooper());
         mTrackerHandler = new NetworkStateTrackerHandler(mHandlerThread.getLooper());
@@ -1892,11 +1913,12 @@
                 && mDeps.isFeatureEnabled(context, REQUEST_RESTRICTED_WIFI);
         mBackgroundFirewallChainEnabled = mDeps.isAtLeastV() && mDeps.isFeatureNotChickenedOut(
                 context, ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN);
-        mUseDeclaredMethodsForCallbacksEnabled = mDeps.isFeatureEnabled(context,
-                ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS);
+        mUseDeclaredMethodsForCallbacksEnabled =
+                mDeps.isFeatureNotChickenedOut(context,
+                        ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS);
         // registerUidFrozenStateChangedCallback is only available on U+
         mQueueCallbacksForFrozenApps = mDeps.isAtLeastU()
-                && mDeps.isFeatureEnabled(context, QUEUE_CALLBACKS_FOR_FROZEN_APPS);
+                && mDeps.isFeatureNotChickenedOut(context, QUEUE_CALLBACKS_FOR_FROZEN_APPS);
         mCarrierPrivilegeAuthenticator = mDeps.makeCarrierPrivilegeAuthenticator(
                 mContext, mTelephonyManager, mRequestRestrictedWifiEnabled,
                 this::handleUidCarrierPrivilegesLost, mHandler);
@@ -2084,6 +2106,8 @@
         }
         mIngressToVpnAddressFiltering = mDeps.isAtLeastT()
                 && mDeps.isFeatureNotChickenedOut(mContext, INGRESS_TO_VPN_ADDRESS_FILTERING);
+
+        mL2capNetworkProvider = mDeps.makeL2capNetworkProvider(mContext);
     }
 
     /**
@@ -4119,6 +4143,10 @@
             mCarrierPrivilegeAuthenticator.start();
         }
 
+        if (mL2capNetworkProvider != null) {
+            mL2capNetworkProvider.start();
+        }
+
         // On T+ devices, register callback for statsd to pull NETWORK_BPF_MAP_INFO atom
         if (mDeps.isAtLeastT()) {
             mBpfNetMaps.setPullAtomCallback(mContext);
@@ -5676,6 +5704,7 @@
         // destroyed pending replacement they will be sent when it is disconnected.
         maybeDisableForwardRulesForDisconnectingNai(nai, false /* sendCallbacks */);
         updateIngressToVpnAddressFiltering(null, nai.linkProperties, nai);
+        updateLocalNetworkAddresses(null, nai.linkProperties);
         try {
             mNetd.networkDestroy(nai.network.getNetId());
         } catch (RemoteException | ServiceSpecificException e) {
@@ -6340,8 +6369,20 @@
         }
     }
 
-    private class CaptivePortalImpl extends ICaptivePortal.Stub {
+    public class CaptivePortalImpl extends ICaptivePortal.Stub implements IBinder.DeathRecipient {
         private final Network mNetwork;
+        // Binder object to track the lifetime of the setDelegateUid caller for cleanup purposes.
+        //
+        // Note that in theory it can happen that there are multiple callers for a given
+        // object. For example, the app that receives the CaptivePortal object from the Intent
+        // fired by startCaptivePortalAppInternal could send the object to another process, or
+        // clone it. Only the first of these objects that calls setDelegateUid will properly
+        // register a death recipient. Calls from the other objects will work, but only the
+        // first object's death will cause the death recipient to fire.
+        // TODO: track all callers by callerBinder instead of CaptivePortalImpl, store callerBinder
+        // in a Set. When the death recipient fires, we can remove the callingBinder from the set,
+        // and when the set is empty, we can clear the delegated UID.
+        private IBinder mDelegateUidCaller;
 
         private CaptivePortalImpl(Network network) {
             mNetwork = network;
@@ -6381,6 +6422,55 @@
             }
         }
 
+        private int handleSetDelegateUid(int uid, @NonNull final IBinder callerBinder) {
+            if (mDelegateUidCaller == null) {
+                mDelegateUidCaller = callerBinder;
+                try {
+                    // While technically unnecessary, it is safe to register a DeathRecipient for
+                    // a cleanup operation (where uid = INVALID_UID).
+                    mDelegateUidCaller.linkToDeath(this, 0);
+                } catch (RemoteException e) {
+                    // remote has died, return early.
+                    return ENOTCONN;
+                }
+            }
+
+            final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(mNetwork);
+            if (nai == null) return ENOENT; // network does not exist anymore.
+            if (nai.isDestroyed()) return ENOENT; // network has already been destroyed.
+
+            // TODO: consider allowing the uid to bypass VPN on all networks before V.
+            if (!mDeps.isAtLeastV()) return EOPNOTSUPP;
+
+            // Check whether there has already been a delegate UID configured, if so, perform
+            // cleanup and disallow bypassing VPN for that UID if no other caller is delegating
+            // this UID.
+            // TODO: consider using exceptions instead of errnos.
+            final int errno = nai.removeCaptivePortalDelegateUid(this);
+            if (errno != 0) return errno;
+
+            // If uid == INVALID_UID, we are done.
+            if (uid == INVALID_UID) return 0;
+            return nai.setCaptivePortalDelegateUid(this, uid);
+        }
+
+        @Override
+        public void setDelegateUid(int uid, @NonNull final IBinder callerBinder,
+                @NonNull final IIntResultListener listener) {
+            Objects.requireNonNull(callerBinder);
+            Objects.requireNonNull(listener);
+            enforceAnyPermissionOf(mContext, NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+
+            mHandler.post(() -> {
+                final int errno = handleSetDelegateUid(uid, callerBinder);
+                try {
+                    listener.onResult(errno);
+                } catch (RemoteException e) {
+                    // remote has died, nothing to do.
+                }
+            });
+        }
+
         @Nullable
         private NetworkMonitorManager getNetworkMonitorManager(final Network network) {
             // getNetworkAgentInfoForNetwork is thread-safe
@@ -6390,6 +6480,13 @@
             // nai.networkMonitor() is thread-safe
             return nai.networkMonitor();
         }
+
+        @Override
+        public void binderDied() {
+            // Cleanup invalid UID and restore the VPN bypass rule. Because mDelegateUidCaller is
+            // never reset, it cannot be null in this context.
+            mHandler.post(() -> handleSetDelegateUid(INVALID_UID, mDelegateUidCaller));
+        }
     }
 
     public boolean avoidBadWifi() {
@@ -6691,7 +6788,7 @@
                     final NetworkOfferInfo offer =
                             findNetworkOfferInfoByCallback((INetworkOfferCallback) msg.obj);
                     if (null != offer) {
-                        handleUnregisterNetworkOffer(offer);
+                        handleUnregisterNetworkOffer(offer, true /* releaseReservations */);
                     }
                     break;
                 }
@@ -7608,17 +7705,23 @@
         }
     }
 
-    private void ensureAllNetworkRequestsHaveType(List<NetworkRequest> requests) {
+    private void ensureAllNetworkRequestsHaveSupportedType(List<NetworkRequest> requests) {
+        final boolean isMultilayerRequest = requests.size() > 1;
         for (int i = 0; i < requests.size(); i++) {
-            ensureNetworkRequestHasType(requests.get(i));
+            ensureNetworkRequestHasSupportedType(requests.get(i), isMultilayerRequest);
         }
     }
 
-    private void ensureNetworkRequestHasType(NetworkRequest request) {
+    private void ensureNetworkRequestHasSupportedType(NetworkRequest request,
+            boolean isMultilayerRequest) {
         if (request.type == NetworkRequest.Type.NONE) {
             throw new IllegalArgumentException(
                     "All NetworkRequests in ConnectivityService must have a type");
         }
+        if (isMultilayerRequest && request.type == NetworkRequest.Type.RESERVATION) {
+            throw new IllegalArgumentException(
+                    "Reservation requests are not supported in multilayer request");
+        }
     }
 
     /**
@@ -7729,6 +7832,28 @@
         }
 
         /**
+         * NetworkCapabilities that were created as part of a NetworkOffer in response to a
+         * RESERVATION request. mReservedCapabilities is null if no current offer matches the
+         * RESERVATION request or if the request is not a RESERVATION. Matching is based on
+         * reservationId.
+         */
+        @Nullable
+        private NetworkCapabilities mReservedCapabilities;
+        @Nullable
+        NetworkCapabilities getReservedCapabilities() {
+            return mReservedCapabilities;
+        }
+
+        void setReservedCapabilities(@NonNull NetworkCapabilities caps) {
+            // This function can only be called once. NetworkCapabilities are never reset as the
+            // reservation is released when the offer disappears.
+            if (mReservedCapabilities != null) {
+                logwtf("ReservedCapabilities can only be set once");
+            }
+            mReservedCapabilities = caps;
+        }
+
+        /**
          * Get the list of UIDs this nri applies to.
          */
         @NonNull
@@ -7748,7 +7873,7 @@
         NetworkRequestInfo(int asUid, @NonNull final List<NetworkRequest> r,
                 @NonNull final NetworkRequest requestForCallback, @Nullable final PendingIntent pi,
                 @Nullable String callingAttributionTag, final int preferenceOrder) {
-            ensureAllNetworkRequestsHaveType(r);
+            ensureAllNetworkRequestsHaveSupportedType(r);
             mRequests = initializeRequests(r);
             mNetworkRequestForCallback = requestForCallback;
             mPendingIntent = pi;
@@ -7782,7 +7907,7 @@
                 @NetworkCallback.Flag int callbackFlags,
                 @Nullable String callingAttributionTag, int declaredMethodsFlags) {
             super();
-            ensureAllNetworkRequestsHaveType(r);
+            ensureAllNetworkRequestsHaveSupportedType(r);
             mRequests = initializeRequests(r);
             mNetworkRequestForCallback = requestForCallback;
             mMessenger = m;
@@ -7802,7 +7927,7 @@
         NetworkRequestInfo(@NonNull final NetworkRequestInfo nri,
                 @NonNull final List<NetworkRequest> r) {
             super();
-            ensureAllNetworkRequestsHaveType(r);
+            ensureAllNetworkRequestsHaveSupportedType(r);
             mRequests = initializeRequests(r);
             mNetworkRequestForCallback = nri.getNetworkRequestForCallback();
             final NetworkAgentInfo satisfier = nri.getSatisfier();
@@ -8088,6 +8213,14 @@
             return PREFERENCE_ORDER_NONE;
         }
 
+        public int getReservationId() {
+            // RESERVATIONs cannot be used in multilayer requests.
+            if (isMultilayerRequest()) return RES_ID_UNSET;
+            final NetworkRequest req = mRequests.get(0);
+            // Non-reservation types return RES_ID_UNSET.
+            return req.networkCapabilities.getReservationId();
+        }
+
         @Override
         public void binderDied() {
             // As an immutable collection, mRequests cannot change by the time the
@@ -8139,6 +8272,7 @@
         flags = maybeAppendDeclaredMethod(flags, CALLBACK_BLK_CHANGED, "BLK", sb);
         flags = maybeAppendDeclaredMethod(flags, CALLBACK_LOCAL_NETWORK_INFO_CHANGED,
                 "LOCALINF", sb);
+        flags = maybeAppendDeclaredMethod(flags, CALLBACK_RESERVED, "RES", sb);
         if (flags != 0) {
             sb.append("|0x").append(Integer.toHexString(flags));
         }
@@ -8343,6 +8477,7 @@
                 enforceNetworkStackOrSettingsPermission();
                 // Fall-through since other checks are the same with normal requests.
             case REQUEST:
+            case RESERVATION:
                 networkCapabilities = new NetworkCapabilities(networkCapabilities);
                 enforceNetworkRequestPermissions(networkCapabilities, callingPackageName,
                         callingAttributionTag, callingUid);
@@ -8781,7 +8916,7 @@
 
     @Override
     public void releaseNetworkRequest(NetworkRequest networkRequest) {
-        ensureNetworkRequestHasType(networkRequest);
+        ensureNetworkRequestHasSupportedType(networkRequest, false /* isMultilayerRequest */);
         mHandler.sendMessage(mHandler.obtainMessage(
                 EVENT_RELEASE_NETWORK_REQUEST, mDeps.getCallingUid(), 0, networkRequest));
     }
@@ -8824,6 +8959,11 @@
         Objects.requireNonNull(score);
         Objects.requireNonNull(caps);
         Objects.requireNonNull(callback);
+        if (caps.hasTransport(TRANSPORT_TEST)) {
+            enforceAnyPermissionOf(mContext, Manifest.permission.MANAGE_TEST_NETWORKS);
+        } else {
+            enforceNetworkFactoryPermission();
+        }
         final boolean yieldToBadWiFi = caps.hasTransport(TRANSPORT_CELLULAR) && !avoidBadWifi();
         final NetworkOffer offer = new NetworkOffer(
                 FullScore.makeProspectiveScore(score, caps, yieldToBadWiFi),
@@ -8862,7 +9002,7 @@
             }
         }
         for (final NetworkOfferInfo noi : toRemove) {
-            handleUnregisterNetworkOffer(noi);
+            handleUnregisterNetworkOffer(noi, true /* releaseReservations */);
         }
         if (DBG) log("unregisterNetworkProvider for " + npi.name);
     }
@@ -9295,7 +9435,7 @@
 
         @Override
         public void binderDied() {
-            mHandler.post(() -> handleUnregisterNetworkOffer(this));
+            mHandler.post(() -> handleUnregisterNetworkOffer(this, true /* releaseReservations */));
         }
     }
 
@@ -9306,6 +9446,18 @@
         return false;
     }
 
+    @Nullable
+    private NetworkRequestInfo maybeGetNriForReservedOffer(NetworkOfferInfo noi) {
+        final int reservationId = noi.offer.caps.getReservationId();
+        if (reservationId == RES_ID_UNSET) return null; // not a reserved offer.
+
+        for (NetworkRequestInfo nri : mNetworkRequests.values()) {
+            if (reservationId == nri.getReservationId()) return nri;
+        }
+        // The reservation was withdrawn or the reserving process died.
+        return null;
+    }
+
     /**
      * Register or update a network offer.
      * @param newOffer The new offer. If the callback member is the same as an existing
@@ -9322,19 +9474,62 @@
             return;
         }
         final NetworkOfferInfo existingOffer = findNetworkOfferInfoByCallback(newOffer.callback);
+
+        // If a reserved offer is updated, ensure the capabilities are not changed. This ensures
+        // that the reserved offer's capabilities match the ones passed by the onReserved callback,
+        // which is sent only once.
+        //
+        // TODO: consider letting the provider change the capabilities of an offer as long as they
+        // continue to satisfy the capabilities that were passed to onReserved. This is not needed
+        // today, but it shouldn't violate the API contract:
+        // - NetworkOffer capabilities are not promises
+        // - The app making a reservation must never assume that the capabilities of the reserved
+        // network are equal to the ones that were passed to onReserved. There will almost always be
+        // other capabilities, for example, those that change at runtime such as VALIDATED or
+        // NOT_SUSPENDED.
+        if (null != existingOffer
+                && existingOffer.offer.caps.getReservationId() != RES_ID_UNSET
+                && existingOffer.offer.caps.getReservationId() != RES_ID_MATCH_ALL_RESERVATIONS
+                && !newOffer.caps.equals(existingOffer.offer.caps)) {
+            // Reserved offers are not allowed to update their NetworkCapabilities.
+            // Doing so will immediately remove the offer from CS and send onUnavailable to the app.
+            handleUnregisterNetworkOffer(existingOffer, true /* releaseReservations */);
+            existingOffer.offer.notifyUnneeded();
+            logwtf("Reserved offers must never update their reserved NetworkCapabilities");
+            return;
+        }
+
+        final NetworkOfferInfo noi = new NetworkOfferInfo(newOffer);
         if (null != existingOffer) {
-            handleUnregisterNetworkOffer(existingOffer);
+            // Do not send onUnavailable for a reserved offer when updating it.
+            handleUnregisterNetworkOffer(existingOffer, false /* releaseReservations */);
             newOffer.migrateFrom(existingOffer.offer);
             if (DBG) {
                 // handleUnregisterNetworkOffer has already logged the old offer
                 log("update offer from providerId " + newOffer.providerId + " new : " + newOffer);
             }
         } else {
+            final NetworkRequestInfo reservationNri = maybeGetNriForReservedOffer(noi);
+            if (reservationNri != null) {
+                // A NetworkRequest is only allowed to trigger a single reserved offer (and
+                // onReserved() callback). All subsequent offers are ignored. This either indicates
+                // a bug in the provider (e.g., responding twice to the same reservation, or
+                // updating the capabilities of a reserved offer), or multiple providers responding
+                // to the same offer (which could happen, but is not useful to the requesting app).
+                if (reservationNri.getReservedCapabilities() != null) {
+                    loge("A reservation can only trigger a single offer; new offer is ignored.");
+                    return;
+                }
+                // Always update the reserved offer before calling callCallbackForRequest.
+                reservationNri.setReservedCapabilities(noi.offer.caps);
+                callCallbackForRequest(
+                        reservationNri, null /*networkAgent*/, CALLBACK_RESERVED, 0 /*arg1*/);
+            }
             if (DBG) {
                 log("register offer from providerId " + newOffer.providerId + " : " + newOffer);
             }
         }
-        final NetworkOfferInfo noi = new NetworkOfferInfo(newOffer);
+
         try {
             noi.offer.callback.asBinder().linkToDeath(noi, 0 /* flags */);
         } catch (RemoteException e) {
@@ -9345,7 +9540,8 @@
         issueNetworkNeeds(noi);
     }
 
-    private void handleUnregisterNetworkOffer(@NonNull final NetworkOfferInfo noi) {
+    private void handleUnregisterNetworkOffer(@NonNull final NetworkOfferInfo noi,
+                    boolean releaseReservations) {
         ensureRunningOnConnectivityServiceThread();
         if (DBG) {
             log("unregister offer from providerId " + noi.offer.providerId + " : " + noi.offer);
@@ -9355,6 +9551,18 @@
         // function may be called twice in a row, but the array will no longer contain
         // the offer.
         if (!mNetworkOffers.remove(noi)) return;
+
+        // If the offer was brought up as a result of a reservation, inform the RESERVATION request
+        // that it has disappeared. There is no need to reset nri.mReservedCapabilities to null, as
+        // CALLBACK_UNAVAIL will cause the request to be torn down. In addition, leaving
+        // nri.mReservedOffer set prevents an additional onReserved() callback in
+        // handleRegisterNetworkOffer() in the case of a migration (which would be ignored as it
+        // follows an onUnavailable).
+        final NetworkRequestInfo nri = maybeGetNriForReservedOffer(noi);
+        if (releaseReservations && nri != null) {
+            handleRemoveNetworkRequest(nri);
+            callCallbackForRequest(nri, null /* networkAgent */, CALLBACK_UNAVAIL, 0 /* arg1 */);
+        }
         noi.offer.callback.asBinder().unlinkToDeath(noi, 0 /* flags */);
     }
 
@@ -9397,6 +9605,8 @@
 
         updateIngressToVpnAddressFiltering(newLp, oldLp, networkAgent);
 
+        updateLocalNetworkAddresses(newLp, oldLp);
+
         updateMtu(newLp, oldLp);
         // TODO - figure out what to do for clat
 //        for (LinkProperties lp : newLp.getStackedLinks()) {
@@ -9576,6 +9786,219 @@
     }
 
     /**
+     * Update Local Network Addresses to LocalNetAccess BPF map.
+     * @param newLp new link properties
+     * @param oldLp old link properties
+     */
+    private void updateLocalNetworkAddresses(@Nullable final LinkProperties newLp,
+            @NonNull final LinkProperties oldLp) {
+
+        // The maps are available only after 25Q2 release
+        if (!BpfNetMaps.isAtLeast25Q2()) {
+            return;
+        }
+
+        final CompareResult<String> interfaceDiff = new CompareResult<>(
+                oldLp != null ? oldLp.getAllInterfaceNames() : null,
+                newLp != null ? newLp.getAllInterfaceNames() : null);
+
+        for (final String iface : interfaceDiff.added) {
+            addLocalAddressesToBpfMap(iface, MULTICAST_AND_BROADCAST_PREFIXES, newLp);
+        }
+        for (final String iface : interfaceDiff.removed) {
+            removeLocalAddressesFromBpfMap(iface, MULTICAST_AND_BROADCAST_PREFIXES, oldLp);
+        }
+
+        // The both list contain current link properties + stacked links for new and old LP.
+        List<LinkProperties> newLinkProperties = new ArrayList<>();
+        List<LinkProperties> oldLinkProperties = new ArrayList<>();
+
+        if (newLp != null) {
+            newLinkProperties.add(newLp);
+            newLinkProperties.addAll(newLp.getStackedLinks());
+        }
+        if (oldLp != null) {
+            oldLinkProperties.add(oldLp);
+            oldLinkProperties.addAll(oldLp.getStackedLinks());
+        }
+
+        // map contains interface name to list of local network prefixes added because of change
+        // in link properties
+        Map<String, List<IpPrefix>> prefixesAddedForInterface = new ArrayMap<>();
+
+        final CompareResult<LinkProperties> linkPropertiesDiff = new CompareResult<>(
+                oldLinkProperties, newLinkProperties);
+
+        for (LinkProperties linkProperty : linkPropertiesDiff.added) {
+            List<IpPrefix> unicastLocalPrefixesToBeAdded = new ArrayList<>();
+            for (LinkAddress linkAddress : linkProperty.getLinkAddresses()) {
+                unicastLocalPrefixesToBeAdded.addAll(
+                        getLocalNetworkPrefixesForAddress(linkAddress));
+            }
+            addLocalAddressesToBpfMap(linkProperty.getInterfaceName(),
+                    unicastLocalPrefixesToBeAdded, linkProperty);
+
+            // adding iterface name -> ip prefixes that we added to map
+            if (!prefixesAddedForInterface.containsKey(linkProperty.getInterfaceName())) {
+                prefixesAddedForInterface.put(linkProperty.getInterfaceName(), new ArrayList<>());
+            }
+            prefixesAddedForInterface.get(linkProperty.getInterfaceName())
+                    .addAll(unicastLocalPrefixesToBeAdded);
+        }
+
+        for (LinkProperties linkProperty : linkPropertiesDiff.removed) {
+            List<IpPrefix> unicastLocalPrefixesToBeRemoved = new ArrayList<>();
+            List<IpPrefix> unicastLocalPrefixesAdded = prefixesAddedForInterface.getOrDefault(
+                    linkProperty.getInterfaceName(), new ArrayList<>());
+
+            for (LinkAddress linkAddress : linkProperty.getLinkAddresses()) {
+                unicastLocalPrefixesToBeRemoved.addAll(
+                        getLocalNetworkPrefixesForAddress(linkAddress));
+            }
+
+            // This is to ensure if 10.0.10.0/24 was added and 10.0.11.0/24 was removed both will
+            // still populate the same prefix of 10.0.0.0/8, which mean we should not remove the
+            // prefix because of removal of 10.0.11.0/24
+            unicastLocalPrefixesToBeRemoved.removeAll(unicastLocalPrefixesAdded);
+
+            removeLocalAddressesFromBpfMap(linkProperty.getInterfaceName(),
+                    new ArrayList<>(unicastLocalPrefixesToBeRemoved), linkProperty);
+        }
+    }
+
+    /**
+     * Filters IpPrefix that are local prefixes and LinkAddress is part of them.
+     * @param linkAddress link address used for filtering
+     * @return list of IpPrefix that are local addresses.
+     */
+    private List<IpPrefix> getLocalNetworkPrefixesForAddress(LinkAddress linkAddress) {
+        List<IpPrefix> localPrefixes = new ArrayList<>();
+        if (linkAddress.isIpv6()) {
+            // For IPv6, if the prefix length is greater than zero then they are part of local
+            // network
+            if (linkAddress.getPrefixLength() != 0) {
+                localPrefixes.add(
+                        new IpPrefix(linkAddress.getAddress(), linkAddress.getPrefixLength()));
+            }
+        } else {
+            // For IPv4, if the linkAddress is part of IpPrefix adding prefix to result.
+            for (IpPrefix ipv4LocalPrefix : IPV4_LOCAL_PREFIXES) {
+                if (ipv4LocalPrefix.containsPrefix(
+                        new IpPrefix(linkAddress.getAddress(), linkAddress.getPrefixLength()))) {
+                    localPrefixes.add(ipv4LocalPrefix);
+                }
+            }
+        }
+        return localPrefixes;
+    }
+
+    /**
+     * Adds list of prefixes(addresses) to local network access map.
+     * @param iface interface name
+     * @param prefixes list of prefixes/addresses
+     * @param lp LinkProperties
+     */
+    private void addLocalAddressesToBpfMap(final String iface, final List<IpPrefix> prefixes,
+                                           @Nullable final LinkProperties lp) {
+        if (!BpfNetMaps.isAtLeast25Q2()) return;
+
+        for (IpPrefix prefix : prefixes) {
+            // Add local dnses allow rule To BpfMap before adding the block rule for prefix
+            addLocalDnsesToBpfMap(iface, prefix, lp);
+            /*
+            Prefix length is used by LPM trie map(local_net_access_map) for performing longest
+            prefix matching, this length represents the maximum number of bits used for matching.
+            The interface index should always be matched which is 32-bit integer. For IPv6, prefix
+            length is calculated by adding the ip address prefix length along with interface index
+            making it (32 + length). IPv4 addresses are stored as ipv4-mapped-ipv6 which implies
+            first 96 bits are common for all ipv4 addresses. Hence, prefix length is calculated as
+            32(interface index) + 96 (common for ipv4-mapped-ipv6) + length.
+             */
+            final int prefixLengthConstant = (prefix.isIPv4() ? (32 + 96) : 32);
+            mBpfNetMaps.addLocalNetAccess(prefixLengthConstant + prefix.getPrefixLength(),
+                    iface, prefix.getAddress(), 0, 0, false);
+
+        }
+
+    }
+
+    /**
+     * Removes list of prefixes(addresses) from local network access map.
+     * @param iface interface name
+     * @param prefixes list of prefixes/addresses
+     * @param lp LinkProperties
+     */
+    private void removeLocalAddressesFromBpfMap(final String iface, final List<IpPrefix> prefixes,
+                                                @Nullable final LinkProperties lp) {
+        if (!BpfNetMaps.isAtLeast25Q2()) return;
+
+        for (IpPrefix prefix : prefixes) {
+            // The reasoning for prefix length is explained in addLocalAddressesToBpfMap()
+            final int prefixLengthConstant = (prefix.isIPv4() ? (32 + 96) : 32);
+            mBpfNetMaps.removeLocalNetAccess(prefixLengthConstant
+                    + prefix.getPrefixLength(), iface, prefix.getAddress(), 0, 0);
+
+            // Also remove the allow rule for dnses included in the prefix after removing the block
+            // rule for prefix.
+            removeLocalDnsesFromBpfMap(iface, prefix, lp);
+        }
+    }
+
+    /**
+     * Adds DNS servers to local network access map, if included in the interface prefix
+     * @param iface interface name
+     * @param prefix IpPrefix
+     * @param lp LinkProperties
+     */
+    private void addLocalDnsesToBpfMap(final String iface, IpPrefix prefix,
+            @Nullable final LinkProperties lp) {
+        if (!BpfNetMaps.isAtLeast25Q2() || lp == null) return;
+
+        for (InetAddress dnsServer : lp.getDnsServers()) {
+            // Adds dns allow rule to LocalNetAccessMap for both TCP and UDP protocol at port 53,
+            // if it is a local dns (ie. it falls in the local prefix range).
+            if (prefix.contains(dnsServer)) {
+                mBpfNetMaps.addLocalNetAccess(getIpv4MappedAddressBitLen(), iface, dnsServer,
+                        IPPROTO_UDP, 53, true);
+                mBpfNetMaps.addLocalNetAccess(getIpv4MappedAddressBitLen(), iface, dnsServer,
+                        IPPROTO_TCP, 53, true);
+            }
+        }
+    }
+
+    /**
+     * Removes DNS servers from local network access map, if included in the interface prefix
+     * @param iface interface name
+     * @param prefix IpPrefix
+     * @param lp LinkProperties
+     */
+    private void removeLocalDnsesFromBpfMap(final String iface, IpPrefix prefix,
+            @Nullable final LinkProperties lp) {
+        if (!BpfNetMaps.isAtLeast25Q2() || lp == null) return;
+
+        for (InetAddress dnsServer : lp.getDnsServers()) {
+            // Removes dns allow rule from LocalNetAccessMap for both TCP and UDP protocol
+            // at port 53, if it is a local dns (ie. it falls in the prefix range).
+            if (prefix.contains(dnsServer)) {
+                mBpfNetMaps.removeLocalNetAccess(getIpv4MappedAddressBitLen(), iface, dnsServer,
+                        IPPROTO_UDP, 53);
+                mBpfNetMaps.removeLocalNetAccess(getIpv4MappedAddressBitLen(), iface, dnsServer,
+                        IPPROTO_TCP, 53);
+            }
+        }
+    }
+
+    /**
+     * Returns total bit length of an Ipv4 mapped address.
+     */
+    private int getIpv4MappedAddressBitLen() {
+        final int ifaceLen = 32; // bit length of interface
+        final int inetAddressLen = 32 + 96; // length of ipv4 mapped addresses
+        final int portProtocolLen = 32;  //16 for port + 16 for protocol;
+        return ifaceLen + inetAddressLen + portProtocolLen;
+    }
+
+    /**
      * Have netd update routes from oldLp to newLp.
      * @return true if routes changed between oldLp and newLp
      */
@@ -10573,9 +10996,9 @@
         return bundle;
     }
 
-    // networkAgent is only allowed to be null if notificationType is
-    // CALLBACK_UNAVAIL. This is because UNAVAIL is about no network being
-    // available, while all other cases are about some particular network.
+    // networkAgent is only allowed to be null if notificationType is CALLBACK_UNAVAIL or
+    // CALLBACK_RESERVED. This is because, per definition, no network is available for UNAVAIL, and
+    // RESERVED callbacks happen when a NetworkOffer is created in response to a reservation.
     private void callCallbackForRequest(@NonNull final NetworkRequestInfo nri,
             @Nullable final NetworkAgentInfo networkAgent, final int notificationType,
             final int arg1) {
@@ -10587,6 +11010,10 @@
         }
         // Even if a callback ends up not being sent, it may affect other callbacks in the queue, so
         // queue callbacks before checking the declared methods flags.
+        // UNAVAIL and RESERVED callbacks are safe not to be queued, because RESERVED must always be
+        // the first callback. In addition, RESERVED cannot be sent more than once and is only
+        // cancelled by UNVAIL.
+        // TODO: evaluate whether it makes sense to queue RESERVED callbacks.
         if (networkAgent != null && nri.maybeQueueCallback(networkAgent, notificationType)) {
             return;
         }
@@ -10594,14 +11021,24 @@
             // No need to send the notification as the recipient method is not overridden
             return;
         }
-        final Network bundleNetwork = notificationType == CALLBACK_UNAVAIL
-                ? null
-                : networkAgent.network;
+        // networkAgent is only null for UNAVAIL and RESERVED.
+        final Network bundleNetwork = (networkAgent != null) ? networkAgent.network : null;
         final Bundle bundle = makeCommonBundleForCallback(nri, bundleNetwork);
         final boolean includeLocationSensitiveInfo =
                 (nri.mCallbackFlags & NetworkCallback.FLAG_INCLUDE_LOCATION_INFO) != 0;
         final NetworkRequest nrForCallback = nri.getNetworkRequestForCallback();
         switch (notificationType) {
+            case CALLBACK_RESERVED: {
+                final NetworkCapabilities nc =
+                        createWithLocationInfoSanitizedIfNecessaryWhenParceled(
+                                networkCapabilitiesRestrictedForCallerPermissions(
+                                        nri.getReservedCapabilities(), nri.mPid, nri.mUid),
+                                includeLocationSensitiveInfo, nri.mPid, nri.mUid,
+                                nrForCallback.getRequestorPackageName(),
+                                nri.mCallingAttributionTag);
+                putParcelable(bundle, nc);
+                break;
+            }
             case CALLBACK_AVAILABLE: {
                 final NetworkCapabilities nc =
                         createWithLocationInfoSanitizedIfNecessaryWhenParceled(
diff --git a/service/src/com/android/server/L2capNetworkProvider.java b/service/src/com/android/server/L2capNetworkProvider.java
new file mode 100644
index 0000000..0352ad5
--- /dev/null
+++ b/service/src/com/android/server/L2capNetworkProvider.java
@@ -0,0 +1,691 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import static android.content.pm.PackageManager.FEATURE_BLUETOOTH_LE;
+import static android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_ANY;
+import static android.net.L2capNetworkSpecifier.ROLE_CLIENT;
+import static android.net.L2capNetworkSpecifier.ROLE_SERVER;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
+import static android.net.NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS;
+import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
+import static android.system.OsConstants.F_GETFL;
+import static android.system.OsConstants.F_SETFL;
+import static android.system.OsConstants.O_NONBLOCK;
+
+import android.annotation.Nullable;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothServerSocket;
+import android.bluetooth.BluetoothSocket;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.L2capNetworkSpecifier;
+import android.net.NetworkCapabilities;
+import android.net.NetworkProvider;
+import android.net.NetworkProvider.NetworkOfferCallback;
+import android.net.NetworkRequest;
+import android.net.NetworkScore;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.ParcelFileDescriptor;
+import android.system.Os;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.HandlerUtils;
+import com.android.net.module.util.ServiceConnectivityJni;
+import com.android.server.net.L2capNetwork;
+import com.android.server.net.L2capNetwork.L2capIpClient;
+import com.android.server.net.L2capPacketForwarder;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+
+public class L2capNetworkProvider {
+    private static final String TAG = L2capNetworkProvider.class.getSimpleName();
+    private static final NetworkCapabilities COMMON_CAPABILITIES =
+            // TODO: add NET_CAPABILITY_NOT_RESTRICTED and check that getRequestorUid() has
+            // BLUETOOTH_CONNECT permission.
+            NetworkCapabilities.Builder.withoutDefaultCapabilities()
+                    .addTransportType(TRANSPORT_BLUETOOTH)
+                    .addCapability(NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
+                    .addCapability(NET_CAPABILITY_NOT_CONGESTED)
+                    .addCapability(NET_CAPABILITY_NOT_METERED)
+                    .addCapability(NET_CAPABILITY_NOT_ROAMING)
+                    .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                    .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+                    .addCapability(NET_CAPABILITY_NOT_VPN)
+                    .build();
+    private final Dependencies mDeps;
+    private final Context mContext;
+    private final HandlerThread mHandlerThread;
+    private final Handler mHandler;
+    private final NetworkProvider mProvider;
+    private final BlanketReservationOffer mBlanketOffer;
+    private final Set<ReservedServerOffer> mReservedServerOffers = new ArraySet<>();
+    private final ClientOffer mClientOffer;
+    // mBluetoothManager guaranteed non-null when read on handler thread after start() is called
+    @Nullable
+    private BluetoothManager mBluetoothManager;
+
+    // Note: IFNAMSIZ is 16.
+    private static final String TUN_IFNAME = "l2cap-tun";
+    private static int sTunIndex = 0;
+
+    /**
+     * The blanket reservation offer is used to create an L2CAP server network, i.e. a network
+     * based on a BluetoothServerSocket.
+     *
+     * Note that NetworkCapabilities matching semantics will cause onNetworkNeeded to be called for
+     * requests that do not have a NetworkSpecifier set.
+     */
+    private class BlanketReservationOffer implements NetworkOfferCallback {
+        public static final NetworkScore SCORE = new NetworkScore.Builder().build();
+        // Note the missing NET_CAPABILITY_NOT_RESTRICTED marking the network as restricted.
+        public static final NetworkCapabilities CAPABILITIES;
+        static {
+            // Below capabilities will match any reservation request with an L2capNetworkSpecifier
+            // that specifies ROLE_SERVER or without a NetworkSpecifier.
+            final L2capNetworkSpecifier l2capNetworkSpecifier = new L2capNetworkSpecifier.Builder()
+                    .setRole(ROLE_SERVER)
+                    .build();
+            NetworkCapabilities caps = new NetworkCapabilities.Builder(COMMON_CAPABILITIES)
+                    .setNetworkSpecifier(l2capNetworkSpecifier)
+                    .build();
+            // TODO: add #setReservationId() to NetworkCapabilities.Builder
+            caps.setReservationId(RES_ID_MATCH_ALL_RESERVATIONS);
+            CAPABILITIES = caps;
+        }
+
+        @Override
+        public void onNetworkNeeded(NetworkRequest request) {
+            // The NetworkSpecifier is guaranteed to be either null or an L2capNetworkSpecifier, so
+            // this cast is safe.
+            final L2capNetworkSpecifier specifier =
+                    (L2capNetworkSpecifier) request.getNetworkSpecifier();
+            if (specifier == null) return;
+            if (!specifier.isValidServerReservationSpecifier()) {
+                Log.i(TAG, "Ignoring invalid reservation request: " + request);
+                return;
+            }
+
+            final ReservedServerOffer reservedOffer = createReservedServerOffer(request);
+            if (reservedOffer == null) {
+                // Something went wrong when creating the offer. Send onUnavailable() to the app.
+                Log.e(TAG, "Failed to create L2cap server offer");
+                mProvider.declareNetworkRequestUnfulfillable(request);
+                return;
+            }
+
+            final NetworkCapabilities reservedCaps = reservedOffer.getReservedCapabilities();
+            mProvider.registerNetworkOffer(SCORE, reservedCaps, mHandler::post, reservedOffer);
+            mReservedServerOffers.add(reservedOffer);
+        }
+
+        @Nullable
+        private ReservedServerOffer createReservedServerOffer(NetworkRequest reservation) {
+            final BluetoothAdapter bluetoothAdapter = mBluetoothManager.getAdapter();
+            if (bluetoothAdapter == null) {
+                Log.w(TAG, "Failed to get BluetoothAdapter");
+                return null;
+            }
+            final BluetoothServerSocket serverSocket;
+            try {
+                serverSocket = bluetoothAdapter.listenUsingInsecureL2capChannel();
+            } catch (IOException e) {
+                Log.w(TAG, "Failed to open BluetoothServerSocket");
+                return null;
+            }
+
+            // Create the reserved capabilities partially from the reservation itself (non-reserved
+            // parts of the L2capNetworkSpecifier), the COMMON_CAPABILITIES, and the reserved data
+            // (BLE L2CAP PSM from the BluetoothServerSocket).
+            final NetworkCapabilities reservationNc = reservation.networkCapabilities;
+            final L2capNetworkSpecifier reservationSpec =
+                    (L2capNetworkSpecifier) reservationNc.getNetworkSpecifier();
+            // Note: the RemoteAddress is unspecified for server networks.
+            final L2capNetworkSpecifier reservedSpec = new L2capNetworkSpecifier.Builder()
+                    .setRole(ROLE_SERVER)
+                    .setHeaderCompression(reservationSpec.getHeaderCompression())
+                    .setPsm(serverSocket.getPsm())
+                    .build();
+            NetworkCapabilities reservedNc =
+                    new NetworkCapabilities.Builder(COMMON_CAPABILITIES)
+                            .setNetworkSpecifier(reservedSpec)
+                            .build();
+            reservedNc.setReservationId(reservationNc.getReservationId());
+            return new ReservedServerOffer(reservedNc, serverSocket);
+        }
+
+        @Nullable
+        private ReservedServerOffer getReservedOfferForRequest(NetworkRequest request) {
+            final int rId = request.networkCapabilities.getReservationId();
+            for (ReservedServerOffer offer : mReservedServerOffers) {
+                // Comparing by reservationId is more explicit then using canBeSatisfiedBy() or the
+                // requestId.
+                if (offer.getReservedCapabilities().getReservationId() != rId) continue;
+                return offer;
+            }
+            return null;
+        }
+
+        @Override
+        public void onNetworkUnneeded(NetworkRequest request) {
+            final ReservedServerOffer reservedOffer = getReservedOfferForRequest(request);
+            if (reservedOffer == null) return;
+
+            // Note that the reserved offer gets torn down when the reservation goes away, even if
+            // there are active (non-reservation) requests for said offer.
+            destroyAndUnregisterReservedOffer(reservedOffer);
+        }
+    }
+
+    private void destroyAndUnregisterReservedOffer(ReservedServerOffer reservedOffer) {
+        HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+        // Ensure the offer still exists if this was posted on the handler.
+        if (!mReservedServerOffers.contains(reservedOffer)) return;
+        mReservedServerOffers.remove(reservedOffer);
+
+        reservedOffer.tearDown();
+        mProvider.unregisterNetworkOffer(reservedOffer);
+    }
+
+    @Nullable
+    private L2capNetwork createL2capNetwork(BluetoothSocket socket, NetworkCapabilities caps,
+            L2capNetwork.ICallback cb) {
+        HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+        final String ifname = TUN_IFNAME + String.valueOf(sTunIndex++);
+        final ParcelFileDescriptor tunFd = mDeps.createTunInterface(ifname);
+        if (tunFd == null) {
+            return null;
+        }
+
+        return L2capNetwork.create(
+                mHandler, mContext, mProvider, ifname, socket, tunFd, caps, mDeps, cb);
+    }
+
+    private static void closeBluetoothSocket(BluetoothSocket socket) {
+        try {
+            socket.close();
+        } catch (IOException e) {
+            Log.w(TAG, "Failed to close BluetoothSocket", e);
+        }
+    }
+
+    private class ReservedServerOffer implements NetworkOfferCallback {
+        private final NetworkCapabilities mReservedCapabilities;
+        private final AcceptThread mAcceptThread;
+        // This set should almost always contain at most one network. This is because all L2CAP
+        // server networks created by the same reserved offer are indistinguishable from each other,
+        // so that ConnectivityService will tear down all but the first. However, temporarily, there
+        // can be more than one network.
+        private final Set<L2capNetwork> mL2capNetworks = new ArraySet<>();
+
+        private class AcceptThread extends Thread {
+            private static final int TIMEOUT_MS = 500;
+            private final BluetoothServerSocket mServerSocket;
+
+            public AcceptThread(BluetoothServerSocket serverSocket) {
+                super("L2capNetworkProvider-AcceptThread");
+                mServerSocket = serverSocket;
+            }
+
+            private void postDestroyAndUnregisterReservedOffer() {
+                // Called on AcceptThread
+                mHandler.post(() -> {
+                    destroyAndUnregisterReservedOffer(ReservedServerOffer.this);
+                });
+            }
+
+            private void postCreateServerNetwork(BluetoothSocket connectedSocket) {
+                // Called on AcceptThread
+                mHandler.post(() -> {
+                    final boolean success = createServerNetwork(connectedSocket);
+                    if (!success) closeBluetoothSocket(connectedSocket);
+                });
+            }
+
+            @Override
+            public void run() {
+                while (true) {
+                    final BluetoothSocket connectedSocket;
+                    try {
+                        connectedSocket = mServerSocket.accept();
+                    } catch (IOException e) {
+                        // Note calling BluetoothServerSocket#close() also triggers an IOException
+                        // which is indistinguishable from any other exceptional behavior.
+                        // postDestroyAndUnregisterReservedOffer() is always safe to call as it
+                        // first checks whether the offer still exists; so if the
+                        // BluetoothServerSocket was closed (i.e. on tearDown()) this is a noop.
+                        Log.w(TAG, "BluetoothServerSocket closed or #accept failed", e);
+                        postDestroyAndUnregisterReservedOffer();
+                        return; // stop running immediately on error
+                    }
+                    postCreateServerNetwork(connectedSocket);
+                }
+            }
+
+            public void tearDown() {
+                HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+                try {
+                    // BluetoothServerSocket.close() is thread-safe.
+                    mServerSocket.close();
+                } catch (IOException e) {
+                    Log.w(TAG, "Failed to close BluetoothServerSocket", e);
+                }
+                try {
+                    join();
+                } catch (InterruptedException e) {
+                    // join() interrupted during tearDown(). Do nothing.
+                }
+            }
+        }
+
+        private boolean createServerNetwork(BluetoothSocket socket) {
+            HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+            // It is possible the offer went away.
+            if (!mReservedServerOffers.contains(this)) return false;
+
+            if (!socket.isConnected()) {
+                Log.wtf(TAG, "BluetoothSocket must be connected");
+                return false;
+            }
+
+            final L2capNetwork network = createL2capNetwork(socket, mReservedCapabilities,
+                    new L2capNetwork.ICallback() {
+                    @Override
+                    public void onError(L2capNetwork network) {
+                        HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+                        destroyAndUnregisterReservedOffer(ReservedServerOffer.this);
+                    }
+                    @Override
+                    public void onNetworkUnwanted(L2capNetwork network) {
+                        HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+                        // Leave reservation in place.
+                        final boolean networkExists = mL2capNetworks.remove(network);
+                        if (!networkExists) return; // already torn down.
+                        network.tearDown();
+                    }
+            });
+
+            if (network == null) {
+                Log.e(TAG, "Failed to create L2capNetwork");
+                return false;
+            }
+
+            mL2capNetworks.add(network);
+            return true;
+        }
+
+        public ReservedServerOffer(NetworkCapabilities reservedCapabilities,
+                BluetoothServerSocket serverSocket) {
+            mReservedCapabilities = reservedCapabilities;
+            mAcceptThread = new AcceptThread(serverSocket);
+            mAcceptThread.start();
+        }
+
+        public NetworkCapabilities getReservedCapabilities() {
+            return mReservedCapabilities;
+        }
+
+        @Override
+        public void onNetworkNeeded(NetworkRequest request) {
+            // UNUSED: the lifetime of the reserved network is controlled by the blanket offer.
+        }
+
+        @Override
+        public void onNetworkUnneeded(NetworkRequest request) {
+            // UNUSED: the lifetime of the reserved network is controlled by the blanket offer.
+        }
+
+        /** Called when the reservation goes away and the reserved offer must be torn down. */
+        public void tearDown() {
+            HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+            mAcceptThread.tearDown();
+            for (L2capNetwork network : mL2capNetworks) {
+                network.tearDown();
+            }
+        }
+    }
+
+    private class ClientOffer implements NetworkOfferCallback {
+        public static final NetworkScore SCORE = new NetworkScore.Builder().build();
+        public static final NetworkCapabilities CAPABILITIES;
+        static {
+            // Below capabilities will match any request with an L2capNetworkSpecifier
+            // that specifies ROLE_CLIENT or without a NetworkSpecifier.
+            final L2capNetworkSpecifier l2capNetworkSpecifier = new L2capNetworkSpecifier.Builder()
+                    .setRole(ROLE_CLIENT)
+                    .build();
+            CAPABILITIES = new NetworkCapabilities.Builder(COMMON_CAPABILITIES)
+                    .setNetworkSpecifier(l2capNetworkSpecifier)
+                    .build();
+        }
+
+        private final Map<L2capNetworkSpecifier, ClientRequestInfo> mClientNetworkRequests =
+                new ArrayMap<>();
+
+        /**
+         * State object to store information for client NetworkRequests.
+         */
+        private static class ClientRequestInfo {
+            public final L2capNetworkSpecifier specifier;
+            public final List<NetworkRequest> requests = new ArrayList<>();
+            // TODO: add support for retries.
+            public final ConnectThread connectThread;
+            @Nullable
+            public L2capNetwork network;
+
+            public ClientRequestInfo(NetworkRequest request, ConnectThread connectThread) {
+                this.specifier = (L2capNetworkSpecifier) request.getNetworkSpecifier();
+                this.requests.add(request);
+                this.connectThread = connectThread;
+            }
+        }
+
+        // TODO: consider using ExecutorService
+        private class ConnectThread extends Thread {
+            private final L2capNetworkSpecifier mSpecifier;
+            private final BluetoothSocket mSocket;
+
+            public ConnectThread(L2capNetworkSpecifier specifier, BluetoothSocket socket) {
+                super("L2capNetworkProvider-ConnectThread");
+                mSpecifier = specifier;
+                mSocket = socket;
+            }
+
+            @Override
+            public void run() {
+                try {
+                    mSocket.connect();
+                    mHandler.post(() -> {
+                        final boolean success = createClientNetwork(mSpecifier, mSocket);
+                        if (!success) closeBluetoothSocket(mSocket);
+                    });
+                } catch (IOException e) {
+                    Log.w(TAG, "BluetoothSocket was closed or #connect failed", e);
+                    // It is safe to call BluetoothSocket#close() multiple times.
+                    closeBluetoothSocket(mSocket);
+                    mHandler.post(() -> {
+                        // Note that if the Socket was closed, this call is a noop as the
+                        // ClientNetworkRequest has already been removed.
+                        declareAllNetworkRequestsUnfulfillable(mSpecifier);
+                    });
+                }
+            }
+
+            public void abort() {
+                HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+                // Closing the BluetoothSocket is the only way to unblock connect() because it calls
+                // shutdown on the underlying (connected) SOCK_SEQPACKET.
+                // It is safe to call BluetoothSocket#close() multiple times.
+                closeBluetoothSocket(mSocket);
+                try {
+                    join();
+                } catch (InterruptedException e) {
+                    Log.i(TAG, "Interrupted while joining ConnectThread", e);
+                }
+            }
+        }
+
+        private boolean createClientNetwork(L2capNetworkSpecifier specifier,
+                BluetoothSocket socket) {
+            HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+            // Check whether request still exists
+            final ClientRequestInfo cri = mClientNetworkRequests.get(specifier);
+            if (cri == null) return false;
+
+            final NetworkCapabilities caps = new NetworkCapabilities.Builder(CAPABILITIES)
+                    .setNetworkSpecifier(specifier)
+                    .build();
+
+            final L2capNetwork network = createL2capNetwork(socket, caps,
+                    new L2capNetwork.ICallback() {
+                    // TODO: do not send onUnavailable() after the network has become available. The
+                    // right thing to do here is to tearDown the network (if it still exists,
+                    // because note that the request might have already been removed in the
+                    // meantime, so `network` cannot be used directly.
+                    @Override
+                    public void onError(L2capNetwork network) {
+                        HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+                        declareAllNetworkRequestsUnfulfillable(specifier);
+                    }
+                    @Override
+                    public void onNetworkUnwanted(L2capNetwork network) {
+                        HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+                        declareAllNetworkRequestsUnfulfillable(specifier);
+                    }
+            });
+            if (network == null) return false;
+
+            cri.network = network;
+            return true;
+        }
+
+        @Override
+        public void onNetworkNeeded(NetworkRequest request) {
+            // The NetworkSpecifier is guaranteed to be either null or an L2capNetworkSpecifier, so
+            // this cast is safe.
+            final L2capNetworkSpecifier requestSpecifier =
+                    (L2capNetworkSpecifier) request.getNetworkSpecifier();
+            if (requestSpecifier == null) return;
+            if (!requestSpecifier.isValidClientRequestSpecifier()) {
+                Log.i(TAG, "Ignoring invalid client request: " + request);
+                return;
+            }
+
+             // Check whether this exact request is already being tracked.
+            final ClientRequestInfo cri = mClientNetworkRequests.get(requestSpecifier);
+            if (cri != null) {
+                Log.d(TAG, "The request is already being tracked. NetworkRequest: " + request);
+                cri.requests.add(request);
+                return;
+            }
+
+            // Check whether a fuzzy match shows a mismatch in header compression by calling
+            // canBeSatisfiedBy().
+            // TODO: Add a copy constructor to L2capNetworkSpecifier.Builder.
+            final L2capNetworkSpecifier matchAnyHeaderCompressionSpecifier =
+                    new L2capNetworkSpecifier.Builder()
+                            .setRole(requestSpecifier.getRole())
+                            .setRemoteAddress(requestSpecifier.getRemoteAddress())
+                            .setPsm(requestSpecifier.getPsm())
+                            .setHeaderCompression(HEADER_COMPRESSION_ANY)
+                            .build();
+            for (L2capNetworkSpecifier existingSpecifier : mClientNetworkRequests.keySet()) {
+                if (existingSpecifier.canBeSatisfiedBy(matchAnyHeaderCompressionSpecifier)) {
+                    // This requeset can never be serviced as this network already exists with a
+                    // different header compression mechanism.
+                    mProvider.declareNetworkRequestUnfulfillable(request);
+                    return;
+                }
+            }
+
+            // If the code reaches here, this is a new request.
+            final BluetoothAdapter bluetoothAdapter = mBluetoothManager.getAdapter();
+            if (bluetoothAdapter == null) {
+                Log.w(TAG, "Failed to get BluetoothAdapter");
+                mProvider.declareNetworkRequestUnfulfillable(request);
+                return;
+            }
+
+            final byte[] macAddress = requestSpecifier.getRemoteAddress().toByteArray();
+            final BluetoothDevice bluetoothDevice = bluetoothAdapter.getRemoteDevice(macAddress);
+            final BluetoothSocket socket;
+            try {
+                socket = bluetoothDevice.createInsecureL2capChannel(requestSpecifier.getPsm());
+            } catch (IOException e) {
+                Log.w(TAG, "Failed to createInsecureL2capChannel", e);
+                mProvider.declareNetworkRequestUnfulfillable(request);
+                return;
+            }
+
+            final ConnectThread connectThread = new ConnectThread(requestSpecifier, socket);
+            connectThread.start();
+            final ClientRequestInfo newRequestInfo = new ClientRequestInfo(request, connectThread);
+            mClientNetworkRequests.put(requestSpecifier, newRequestInfo);
+        }
+
+        @Override
+        public void onNetworkUnneeded(NetworkRequest request) {
+            final L2capNetworkSpecifier specifier =
+                    (L2capNetworkSpecifier) request.getNetworkSpecifier();
+
+            // Map#get() is safe to call with null key
+            final ClientRequestInfo cri = mClientNetworkRequests.get(specifier);
+            if (cri == null) return;
+
+            cri.requests.remove(request);
+            if (cri.requests.size() > 0) return;
+
+            // If the code reaches here, the network needs to be torn down.
+            releaseClientNetworkRequest(cri);
+        }
+
+        /**
+         * Release the client network request and tear down all associated state.
+         *
+         * Only call this when all associated NetworkRequests have been released.
+         */
+        private void releaseClientNetworkRequest(ClientRequestInfo cri) {
+            HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+            mClientNetworkRequests.remove(cri.specifier);
+            if (cri.connectThread.isAlive()) {
+                // Note that if ConnectThread succeeds between calling #isAlive() and #abort(), the
+                // request will already be removed from mClientNetworkRequests by the time the
+                // createClientNetwork() call is processed on the handler, so it is safe to call
+                // #abort().
+                cri.connectThread.abort();
+            }
+
+            if (cri.network != null) {
+                cri.network.tearDown();
+            }
+        }
+
+        private void declareAllNetworkRequestsUnfulfillable(L2capNetworkSpecifier specifier) {
+            HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+            final ClientRequestInfo cri = mClientNetworkRequests.get(specifier);
+            if (cri == null) return;
+
+            for (NetworkRequest request : cri.requests) {
+                mProvider.declareNetworkRequestUnfulfillable(request);
+            }
+            releaseClientNetworkRequest(cri);
+        }
+    }
+
+    @VisibleForTesting
+    public static class Dependencies {
+        /** Get the HandlerThread for L2capNetworkProvider to run on */
+        public HandlerThread getHandlerThread() {
+            final HandlerThread thread = new HandlerThread("L2capNetworkProviderThread");
+            thread.start();
+            return thread;
+        }
+
+        /** Create a tun interface configured for blocking i/o */
+        @Nullable
+        public ParcelFileDescriptor createTunInterface(String ifname) {
+            final ParcelFileDescriptor fd;
+            try {
+                fd = ParcelFileDescriptor.adoptFd(ServiceConnectivityJni.createTunTap(
+                        true /*isTun*/,
+                        true /*hasCarrier*/,
+                        true /*setIffMulticast*/,
+                        ifname));
+                ServiceConnectivityJni.bringUpInterface(ifname);
+                // TODO: consider adding a parameter to createTunTap() (or the Builder that should
+                // be added) to configure i/o blocking.
+                final int flags = Os.fcntlInt(fd.getFileDescriptor(), F_GETFL, 0);
+                Os.fcntlInt(fd.getFileDescriptor(), F_SETFL, flags & ~O_NONBLOCK);
+            } catch (Exception e) {
+                // Note: createTunTap currently throws an IllegalStateException on failure.
+                // TODO: native functions should throw ErrnoException.
+                Log.e(TAG, "Failed to create tun interface", e);
+                return null;
+            }
+            return fd;
+        }
+
+        /** Create an L2capPacketForwarder and start forwarding */
+        public L2capPacketForwarder createL2capPacketForwarder(Handler handler,
+                ParcelFileDescriptor tunFd, BluetoothSocket socket, boolean compressHeaders,
+                L2capPacketForwarder.ICallback cb) {
+            return new L2capPacketForwarder(handler, tunFd, socket, compressHeaders, cb);
+        }
+
+        /** Create an L2capIpClient */
+        public L2capIpClient createL2capIpClient(String logTag, Context context, String ifname) {
+            return new L2capIpClient(logTag, context, ifname);
+        }
+    }
+
+    public L2capNetworkProvider(Context context) {
+        this(new Dependencies(), context);
+    }
+
+    public L2capNetworkProvider(Dependencies deps, Context context) {
+        mDeps = deps;
+        mContext = context;
+        mHandlerThread = mDeps.getHandlerThread();
+        mHandler = new Handler(mHandlerThread.getLooper());
+        mProvider = new NetworkProvider(context, mHandlerThread.getLooper(), TAG);
+        mBlanketOffer = new BlanketReservationOffer();
+        mClientOffer = new ClientOffer();
+    }
+
+    /**
+     * Start L2capNetworkProvider.
+     *
+     * Called on CS Handler thread.
+     */
+    public void start() {
+        mHandler.post(() -> {
+            final PackageManager pm = mContext.getPackageManager();
+            if (!pm.hasSystemFeature(FEATURE_BLUETOOTH_LE)) {
+                return;
+            }
+            mBluetoothManager = mContext.getSystemService(BluetoothManager.class);
+            if (mBluetoothManager == null) {
+                // Can this ever happen?
+                Log.wtf(TAG, "BluetoothManager not found");
+                return;
+            }
+            mContext.getSystemService(ConnectivityManager.class).registerNetworkProvider(mProvider);
+            mProvider.registerNetworkOffer(BlanketReservationOffer.SCORE,
+                    BlanketReservationOffer.CAPABILITIES, mHandler::post, mBlanketOffer);
+            mProvider.registerNetworkOffer(ClientOffer.SCORE,
+                    ClientOffer.CAPABILITIES, mHandler::post, mClientOffer);
+        });
+    }
+}
diff --git a/service/src/com/android/server/TestNetworkService.java b/service/src/com/android/server/TestNetworkService.java
index 4d39d7d..96f4e20 100644
--- a/service/src/com/android/server/TestNetworkService.java
+++ b/service/src/com/android/server/TestNetworkService.java
@@ -48,6 +48,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.net.module.util.NetworkStackConstants;
+import com.android.net.module.util.ServiceConnectivityJni;
 
 import java.io.IOException;
 import java.io.UncheckedIOException;
@@ -75,15 +76,6 @@
     @NonNull private final ConnectivityManager mCm;
     @NonNull private final NetworkProvider mNetworkProvider;
 
-    // Native method stubs
-    private static native int nativeCreateTunTap(boolean isTun, boolean hasCarrier,
-            boolean setIffMulticast, @NonNull String iface);
-
-    private static native void nativeSetTunTapCarrierEnabled(@NonNull String iface, int tunFd,
-            boolean enabled);
-
-    private static native void nativeBringUpInterface(String iface);
-
     @VisibleForTesting
     protected TestNetworkService(@NonNull Context context) {
         mHandlerThread = new HandlerThread("TestNetworkServiceThread");
@@ -143,7 +135,8 @@
             // flags atomically.
             final boolean setIffMulticast = bringUp;
             ParcelFileDescriptor tunIntf = ParcelFileDescriptor.adoptFd(
-                    nativeCreateTunTap(isTun, hasCarrier, setIffMulticast, interfaceName));
+                    ServiceConnectivityJni.createTunTap(
+                            isTun, hasCarrier, setIffMulticast, interfaceName));
 
             // Disable DAD and remove router_solicitation_delay before assigning link addresses.
             if (disableIpv6ProvisioningDelay) {
@@ -160,7 +153,7 @@
             }
 
             if (bringUp) {
-                nativeBringUpInterface(interfaceName);
+                ServiceConnectivityJni.bringUpInterface(interfaceName);
             }
 
             return new TestNetworkInterface(tunIntf, interfaceName);
@@ -403,11 +396,11 @@
     @Override
     public void setCarrierEnabled(@NonNull TestNetworkInterface iface, boolean enabled) {
         enforceTestNetworkPermissions(mContext);
-        nativeSetTunTapCarrierEnabled(iface.getInterfaceName(), iface.getFileDescriptor().getFd(),
-                enabled);
+        ServiceConnectivityJni.setTunTapCarrierEnabled(iface.getInterfaceName(),
+                iface.getFileDescriptor().getFd(), enabled);
         // Explicitly close fd after use to prevent StrictMode from complaining.
         // Also, explicitly referencing iface guarantees that the object is not garbage collected
-        // before nativeSetTunTapCarrierEnabled() executes.
+        // before setTunTapCarrierEnabled() executes.
         try {
             iface.getFileDescriptor().close();
         } catch (IOException e) {
diff --git a/service/src/com/android/server/connectivity/ConnectivityFlags.java b/service/src/com/android/server/connectivity/ConnectivityFlags.java
index 93335f1..136ea81 100644
--- a/service/src/com/android/server/connectivity/ConnectivityFlags.java
+++ b/service/src/com/android/server/connectivity/ConnectivityFlags.java
@@ -26,6 +26,11 @@
  */
 public final class ConnectivityFlags {
     /**
+     * Boot namespace for this module. Values from this should only be read at boot.
+     */
+    public static final String NAMESPACE_TETHERING_BOOT = "tethering_boot";
+
+    /**
      * Minimum module version at which to avoid rematching all requests when a network request is
      * registered, and rematch only the registered requests instead.
      */
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index 94b655f..e762a8e 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -25,6 +25,12 @@
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.NetworkCapabilities.transportNamesOf;
+import static android.system.OsConstants.EIO;
+import static android.system.OsConstants.EEXIST;
+import static android.system.OsConstants.ENOENT;
+
+import static com.android.net.module.util.FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED;
+import static com.android.net.module.util.FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_DISALLOW_BYPASS_VPN_FOR_DELEGATE_UID_ENOENT;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -57,9 +63,11 @@
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.os.ServiceSpecificException;
 import android.os.SystemClock;
 import android.telephony.data.EpsBearerQosSessionAttributes;
 import android.telephony.data.NrQosSessionAttributes;
+import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.Pair;
@@ -68,8 +76,10 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.WakeupMessage;
+import com.android.net.module.util.FrameworkConnectivityStatsLog;
 import com.android.net.module.util.HandlerUtils;
 import com.android.server.ConnectivityService;
+import com.android.server.ConnectivityService.CaptivePortalImpl;
 
 import java.io.PrintWriter;
 import java.net.Inet4Address;
@@ -574,6 +584,10 @@
     // For fast lookups. Indexes into mInactivityTimers by request ID.
     private final SparseArray<InactivityTimer> mInactivityTimerForRequest = new SparseArray<>();
 
+    // Map of delegated UIDs used to bypass VPN and its captive portal app caller.
+    private final ArrayMap<CaptivePortalImpl, Integer> mCaptivePortalDelegateUids =
+            new ArrayMap<>();
+
     // Inactivity expiry timer. Armed whenever mInactivityTimers is non-empty, regardless of
     // whether the network is inactive or not. Always set to the expiry of the mInactivityTimers
     // that expires last. When the timer fires, all inactivity state is cleared, and if the network
@@ -626,6 +640,7 @@
     private final Context mContext;
     private final Handler mHandler;
     private final QosCallbackTracker mQosCallbackTracker;
+    private final INetd mNetd;
 
     private final long mCreationTime;
 
@@ -655,6 +670,7 @@
         mConnServiceDeps = deps;
         setScore(score); // uses members connService, networkCapabilities and networkAgentConfig
         clatd = new Nat464Xlat(this, netd, dnsResolver, deps);
+        mNetd = netd;
         mContext = context;
         mHandler = handler;
         this.factorySerialNumber = factorySerialNumber;
@@ -1112,6 +1128,7 @@
         int delta = add ? +1 : -1;
         switch (request.type) {
             case REQUEST:
+            case RESERVATION:
                 mNumRequestNetworkRequests += delta;
                 break;
 
@@ -1549,6 +1566,58 @@
         }
     }
 
+    private int allowBypassVpnOnNetwork(boolean allow, int uid, int netId) {
+        try {
+            mNetd.networkAllowBypassVpnOnNetwork(allow, uid, netId);
+            return 0;
+        } catch (RemoteException e) {
+            // Netd has crashed, and this process is about to crash as well.
+            return EIO;
+        } catch (ServiceSpecificException e) {
+            return e.errorCode;
+        }
+    }
+
+    /**
+     * Set the delegate UID of the app that is allowed to perform network traffic for captive
+     * portal login, and configure the netd bypass rule with this delegated UID.
+     *
+     * @param caller the captive portal app to that delegated UID
+     * @param uid the delegated UID of the captive portal app.
+     * @return Return 0 if set the UID and VPN bypass rule successfully or bypass rule corresponding
+     *                to this UID already exists otherwise return errno.
+     */
+    public int setCaptivePortalDelegateUid(@NonNull final CaptivePortalImpl caller, int uid) {
+        final int errorCode = allowBypassVpnOnNetwork(true /* allow */, uid, network.netId);
+        if (errorCode == 0 || errorCode == EEXIST) {
+            mCaptivePortalDelegateUids.put(caller, uid);
+        }
+        return errorCode == EEXIST ? 0 : errorCode;
+    }
+
+    /**
+     * Remove the delegate UID of the app that is allowed to perform network traffic for captive
+     * portal login, and remove the netd bypass rule if no other caller is delegating this UID.
+     *
+     * @param caller the captive portal app to that delegated UID.
+     * @return Return 0 if remove the UID and VPN bypass rule successfully or bypass rule
+     *                corresponding to this UID doesn't exist otherwise return errno.
+     */
+    public int removeCaptivePortalDelegateUid(@NonNull final CaptivePortalImpl caller) {
+        final Integer maybeDelegateUid = mCaptivePortalDelegateUids.remove(caller);
+        if (maybeDelegateUid == null) return 0;
+        if (mCaptivePortalDelegateUids.values().contains(maybeDelegateUid)) return 0;
+        final int errorCode =
+                allowBypassVpnOnNetwork(false /* allow */, maybeDelegateUid, network.netId);
+        if (errorCode == ENOENT) {
+            FrameworkConnectivityStatsLog.write(
+                    CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+                    CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_DISALLOW_BYPASS_VPN_FOR_DELEGATE_UID_ENOENT
+            );
+        }
+        return errorCode == ENOENT ? 0 : errorCode;
+    }
+
     private static boolean areAllowedUidsAcceptableFromNetworkAgent(
             @NonNull final NetworkCapabilities nc, final boolean hasAutomotiveFeature,
             @NonNull final ConnectivityService.Dependencies deps,
diff --git a/service/src/com/android/server/connectivity/NetworkOffer.java b/service/src/com/android/server/connectivity/NetworkOffer.java
index eea382e..d294046 100644
--- a/service/src/com/android/server/connectivity/NetworkOffer.java
+++ b/service/src/com/android/server/connectivity/NetworkOffer.java
@@ -42,6 +42,7 @@
  * @hide
  */
 public class NetworkOffer implements NetworkRanker.Scoreable {
+    private static final String TAG = NetworkOffer.class.getSimpleName();
     @NonNull public final FullScore score;
     @NonNull public final NetworkCapabilities caps;
     @NonNull public final INetworkOfferCallback callback;
@@ -126,6 +127,23 @@
     }
 
     /**
+     * Sends onNetworkUnneeded for any remaining NetworkRequests.
+     *
+     * Used after a NetworkOffer migration failed to let the provider know that its networks should
+     * be torn down (as the offer is no longer registered).
+     */
+    public void notifyUnneeded() {
+        try {
+            for (NetworkRequest request : mCurrentlyNeeded) {
+                callback.onNetworkUnneeded(request);
+            }
+        } catch (RemoteException e) {
+            // The remote is dead; nothing to do.
+        }
+        mCurrentlyNeeded.clear();
+    }
+
+    /**
      * Migrate from, and take over, a previous offer.
      *
      * When an updated offer is sent from a provider, call this method on the new offer, passing
diff --git a/service/src/com/android/server/connectivity/NetworkPermissions.java b/service/src/com/android/server/connectivity/NetworkPermissions.java
new file mode 100644
index 0000000..9543d8f
--- /dev/null
+++ b/service/src/com/android/server/connectivity/NetworkPermissions.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import android.net.INetd;
+
+/**
+ * A wrapper class for managing network and traffic permissions.
+ *
+ * This class encapsulates permissions represented as a bitmask, as defined in INetd.aidl
+ * and used within PermissionMonitor.java.  It distinguishes between two types of permissions:
+ *
+ * 1. Network Permissions: These permissions, declared in INetd.aidl, are used
+ *    by the Android platform's network daemon (system/netd) to control network
+ *    management
+ *
+ * 2. Traffic Permissions: These permissions are used internally by PermissionMonitor.java and
+ *    BpfNetMaps.java to manage fine-grained network traffic filtering and control.
+ *
+ * This wrapper ensures that no new permission definitions, here or in aidl, conflict with any
+ * existing permissions. This prevents unintended interactions or overrides.
+ *
+ * @hide
+ */
+public class NetworkPermissions {
+
+    /*
+     * Below are network permissions declared in INetd.aidl and used by the platform. Using these is
+     * equivalent to using the values in android.net.INetd.
+     */
+    public static final int PERMISSION_NONE = INetd.PERMISSION_NONE; /* 0 */
+    public static final int PERMISSION_NETWORK = INetd.PERMISSION_NETWORK; /* 1 */
+    public static final int PERMISSION_SYSTEM = INetd.PERMISSION_SYSTEM; /* 2 */
+
+    /*
+     * Below are traffic permissions used by PermissionMonitor and BpfNetMaps.
+     */
+
+    /**
+     * PERMISSION_UNINSTALLED is used when an app is uninstalled from the device. All internet
+     * related permissions need to be cleaned.
+     */
+    public static final int TRAFFIC_PERMISSION_UNINSTALLED = -1;
+
+    /**
+     * PERMISSION_INTERNET indicates that the app can create AF_INET and AF_INET6 sockets.
+     */
+    public static final int TRAFFIC_PERMISSION_INTERNET = 4;
+
+    /**
+     * PERMISSION_UPDATE_DEVICE_STATS is used for system UIDs and privileged apps
+     * that have the UPDATE_DEVICE_STATS permission.
+     */
+    public static final int TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS = 8;
+
+    /**
+     * TRAFFIC_PERMISSION_SDKSANDBOX_LOCALHOST indicates if an SdkSandbox UID will be allowed
+     * to connect to localhost. For non SdkSandbox UIDs this bit is a no-op.
+     */
+    public static final int TRAFFIC_PERMISSION_SDKSANDBOX_LOCALHOST = 16;
+}
diff --git a/service/src/com/android/server/connectivity/PermissionMonitor.java b/service/src/com/android/server/connectivity/PermissionMonitor.java
index beaa174..5de5f61 100755
--- a/service/src/com/android/server/connectivity/PermissionMonitor.java
+++ b/service/src/com/android/server/connectivity/PermissionMonitor.java
@@ -19,25 +19,29 @@
 import static android.Manifest.permission.CHANGE_NETWORK_STATE;
 import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
 import static android.Manifest.permission.INTERNET;
+import static android.Manifest.permission.NEARBY_WIFI_DEVICES;
 import static android.Manifest.permission.NETWORK_STACK;
 import static android.Manifest.permission.UPDATE_DEVICE_STATS;
 import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED;
 import static android.content.pm.PackageManager.GET_PERMISSIONS;
 import static android.net.ConnectivitySettingsManager.UIDS_ALLOWED_ON_RESTRICTED_NETWORKS;
-import static android.net.INetd.PERMISSION_INTERNET;
-import static android.net.INetd.PERMISSION_NETWORK;
-import static android.net.INetd.PERMISSION_NONE;
-import static android.net.INetd.PERMISSION_SYSTEM;
-import static android.net.INetd.PERMISSION_UNINSTALLED;
-import static android.net.INetd.PERMISSION_UPDATE_DEVICE_STATS;
 import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
+import static android.net.connectivity.ConnectivityCompatChanges.RESTRICT_LOCAL_NETWORK;
 import static android.os.Process.INVALID_UID;
 import static android.os.Process.SYSTEM_UID;
 
+import static com.android.server.connectivity.NetworkPermissions.PERMISSION_NETWORK;
+import static com.android.server.connectivity.NetworkPermissions.PERMISSION_NONE;
+import static com.android.server.connectivity.NetworkPermissions.PERMISSION_SYSTEM;
+import static com.android.server.connectivity.NetworkPermissions.TRAFFIC_PERMISSION_INTERNET;
+import static com.android.server.connectivity.NetworkPermissions.TRAFFIC_PERMISSION_UNINSTALLED;
+import static com.android.server.connectivity.NetworkPermissions.TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS;
 import static com.android.net.module.util.CollectionUtils.toIntArray;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.compat.CompatChanges;
+import android.content.AttributionSource;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -60,6 +64,7 @@
 import android.os.SystemConfigManager;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.permission.PermissionManager;
 import android.provider.Settings;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -96,6 +101,8 @@
     private final PackageManager mPackageManager;
     private final UserManager mUserManager;
     private final SystemConfigManager mSystemConfigManager;
+    private final PermissionManager mPermissionManager;
+    private final PermissionChangeListener mPermissionChangeListener;
     private final INetd mNetd;
     private final Dependencies mDeps;
     private final Context mContext;
@@ -227,6 +234,12 @@
             context.getContentResolver().registerContentObserver(
                     uri, notifyForDescendants, observer);
         }
+
+        public boolean shouldEnforceLocalNetRestrictions(int uid) {
+            // TODO(b/394567896): Update compat change checks for enforcement
+            return BpfNetMaps.isAtLeast25Q2() &&
+                    CompatChanges.isChangeEnabled(RESTRICT_LOCAL_NETWORK, uid);
+        }
     }
 
     private static class MultiSet<T> {
@@ -267,18 +280,49 @@
     }
 
     @VisibleForTesting
-    PermissionMonitor(@NonNull final Context context, @NonNull final INetd netd,
+    public PermissionMonitor(@NonNull final Context context, @NonNull final INetd netd,
             @NonNull final BpfNetMaps bpfNetMaps,
             @NonNull final Dependencies deps,
             @NonNull final HandlerThread thread) {
         mPackageManager = context.getPackageManager();
         mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
         mSystemConfigManager = context.getSystemService(SystemConfigManager.class);
+        mPermissionManager = context.getSystemService(PermissionManager.class);
+        mPermissionChangeListener = new PermissionChangeListener();
         mNetd = netd;
         mDeps = deps;
         mContext = context;
         mBpfNetMaps = bpfNetMaps;
         mThread = thread;
+        if (BpfNetMaps.isAtLeast25Q2()) {
+            // Local net restrictions is supported as a developer opt-in starting in Android B.
+            // This listener should finish registration by the time the system has completed
+            // boot setup such that any changes to runtime permissions for local network
+            // restrictions can only occur after this registration has completed.
+            mPackageManager.addOnPermissionsChangeListener(mPermissionChangeListener);
+        }
+    }
+
+    @VisibleForTesting
+    void setLocalNetworkPermissions(final int uid, @Nullable final String packageName) {
+        if (!mDeps.shouldEnforceLocalNetRestrictions(uid)) return;
+
+        final AttributionSource attributionSource =
+                new AttributionSource.Builder(uid).setPackageName(packageName).build();
+        final int permissionState = mPermissionManager.checkPermissionForPreflight(
+                NEARBY_WIFI_DEVICES, attributionSource);
+        if (permissionState == PermissionManager.PERMISSION_GRANTED) {
+            mBpfNetMaps.removeUidFromLocalNetBlockMap(attributionSource.getUid());
+        } else {
+            mBpfNetMaps.addUidToLocalNetBlockMap(attributionSource.getUid());
+        }
+        if (hasSdkSandbox(uid)){
+            // SDKs in the SDK RT cannot hold runtime permissions
+            final int sdkSandboxUid = sProcessShim.toSdkSandboxUid(uid);
+            if (!mBpfNetMaps.isUidBlockedFromUsingLocalNetwork(sdkSandboxUid)) {
+                mBpfNetMaps.addUidToLocalNetBlockMap(sdkSandboxUid);
+            }
+        }
     }
 
     private void ensureRunningOnHandlerThread() {
@@ -341,6 +385,7 @@
                     uidsPerm.put(sdkSandboxUid, permission);
                 }
             }
+            setLocalNetworkPermissions(uid, app.packageName);
         }
         return uidsPerm;
     }
@@ -395,7 +440,7 @@
         final SparseIntArray appIdsPerm = new SparseIntArray();
         for (final int uid : mSystemConfigManager.getSystemPermissionUids(INTERNET)) {
             final int appId = UserHandle.getAppId(uid);
-            final int permission = appIdsPerm.get(appId) | PERMISSION_INTERNET;
+            final int permission = appIdsPerm.get(appId) | TRAFFIC_PERMISSION_INTERNET;
             appIdsPerm.put(appId, permission);
             if (hasSdkSandbox(appId)) {
                 appIdsPerm.put(sProcessShim.toSdkSandboxUid(appId), permission);
@@ -403,7 +448,7 @@
         }
         for (final int uid : mSystemConfigManager.getSystemPermissionUids(UPDATE_DEVICE_STATS)) {
             final int appId = UserHandle.getAppId(uid);
-            final int permission = appIdsPerm.get(appId) | PERMISSION_UPDATE_DEVICE_STATS;
+            final int permission = appIdsPerm.get(appId) | TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS;
             appIdsPerm.put(appId, permission);
             if (hasSdkSandbox(appId)) {
                 appIdsPerm.put(sProcessShim.toSdkSandboxUid(appId), permission);
@@ -588,7 +633,7 @@
 
         final List<PackageInfo> apps = getInstalledPackagesAsUser(user);
 
-        // Save all apps
+        // Save all apps in mAllApps
         updateAllApps(apps);
 
         // Uids network permissions
@@ -625,6 +670,11 @@
             final int uid = allUids.keyAt(i);
             if (user.equals(UserHandle.getUserHandleForUid(uid))) {
                 mUidToNetworkPerm.delete(uid);
+                if (mDeps.shouldEnforceLocalNetRestrictions(uid)) {
+                    mBpfNetMaps.removeUidFromLocalNetBlockMap(uid);
+                    if (hasSdkSandbox(uid)) mBpfNetMaps.removeUidFromLocalNetBlockMap(
+                            sProcessShim.toSdkSandboxUid(uid));
+                }
                 removedUids.put(uid, allUids.valueAt(i));
             }
         }
@@ -646,7 +696,7 @@
             final int appId = removedUserAppIds.keyAt(i);
             // Need to clear permission if the removed appId is not found in the array.
             if (appIds.indexOfKey(appId) < 0) {
-                appIds.put(appId, PERMISSION_UNINSTALLED);
+                appIds.put(appId, TRAFFIC_PERMISSION_UNINSTALLED);
             }
         }
         sendAppIdsTrafficPermission(appIds);
@@ -698,7 +748,7 @@
             }
         } else {
             // The last package of this uid is removed from device. Clean the package up.
-            permission = PERMISSION_UNINSTALLED;
+            permission = TRAFFIC_PERMISSION_UNINSTALLED;
         }
         return permission;
     }
@@ -741,13 +791,13 @@
                 return "NETWORK";
             case PERMISSION_SYSTEM:
                 return "SYSTEM";
-            case PERMISSION_INTERNET:
+            case TRAFFIC_PERMISSION_INTERNET:
                 return "INTERNET";
-            case PERMISSION_UPDATE_DEVICE_STATS:
+            case TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS:
                 return "UPDATE_DEVICE_STATS";
-            case (PERMISSION_INTERNET | PERMISSION_UPDATE_DEVICE_STATS):
+            case (TRAFFIC_PERMISSION_INTERNET | TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS):
                 return "ALL";
-            case PERMISSION_UNINSTALLED:
+            case TRAFFIC_PERMISSION_UNINSTALLED:
                 return "UNINSTALLED";
             default:
                 return "UNKNOWN";
@@ -766,7 +816,7 @@
         // (PERMISSION_UNINSTALLED), remove the appId from the array. Otherwise, update the latest
         // permission to the appId.
         final int appId = UserHandle.getAppId(uid);
-        if (uidTrafficPerm == PERMISSION_UNINSTALLED) {
+        if (uidTrafficPerm == TRAFFIC_PERMISSION_UNINSTALLED) {
             userTrafficPerms.delete(appId);
         } else {
             userTrafficPerms.put(appId, uidTrafficPerm);
@@ -784,7 +834,7 @@
                 installed = true;
             }
         }
-        return installed ? permission : PERMISSION_UNINSTALLED;
+        return installed ? permission : TRAFFIC_PERMISSION_UNINSTALLED;
     }
 
     /**
@@ -819,6 +869,7 @@
             }
             sendUidsNetworkPermission(apps, true /* add */);
         }
+        setLocalNetworkPermissions(uid, packageName);
 
         // If the newly-installed package falls within some VPN's uid range, update Netd with it.
         // This needs to happen after the mUidToNetworkPerm update above, since
@@ -863,6 +914,11 @@
     synchronized void onPackageRemoved(@NonNull final String packageName, final int uid) {
         // Update uid permission.
         updateAppIdTrafficPermission(uid);
+        if (BpfNetMaps.isAtLeast25Q2()) {
+            mBpfNetMaps.removeUidFromLocalNetBlockMap(uid);
+            if (hasSdkSandbox(uid)) mBpfNetMaps.removeUidFromLocalNetBlockMap(
+                    sProcessShim.toSdkSandboxUid(uid));
+        }
         // Get the appId permission from all users then send the latest permission to netd.
         final int appId = UserHandle.getAppId(uid);
         final int appIdTrafficPerm = getAppIdTrafficPermission(appId);
@@ -921,11 +977,11 @@
         for (int i = 0; i < requestedPermissions.length; i++) {
             if (requestedPermissions[i].equals(INTERNET)
                     && ((requestedPermissionsFlags[i] & REQUESTED_PERMISSION_GRANTED) != 0)) {
-                permissions |= PERMISSION_INTERNET;
+                permissions |= TRAFFIC_PERMISSION_INTERNET;
             }
             if (requestedPermissions[i].equals(UPDATE_DEVICE_STATS)
                     && ((requestedPermissionsFlags[i] & REQUESTED_PERMISSION_GRANTED) != 0)) {
-                permissions |= PERMISSION_UPDATE_DEVICE_STATS;
+                permissions |= TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS;
             }
         }
         return permissions;
@@ -1164,19 +1220,19 @@
         for (int i = 0; i < netdPermissionsAppIds.size(); i++) {
             int permissions = netdPermissionsAppIds.valueAt(i);
             switch(permissions) {
-                case (PERMISSION_INTERNET | PERMISSION_UPDATE_DEVICE_STATS):
+                case (TRAFFIC_PERMISSION_INTERNET | TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS):
                     allPermissionAppIds.add(netdPermissionsAppIds.keyAt(i));
                     break;
-                case PERMISSION_INTERNET:
+                case TRAFFIC_PERMISSION_INTERNET:
                     internetPermissionAppIds.add(netdPermissionsAppIds.keyAt(i));
                     break;
-                case PERMISSION_UPDATE_DEVICE_STATS:
+                case TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS:
                     updateStatsPermissionAppIds.add(netdPermissionsAppIds.keyAt(i));
                     break;
                 case PERMISSION_NONE:
                     noPermissionAppIds.add(netdPermissionsAppIds.keyAt(i));
                     break;
-                case PERMISSION_UNINSTALLED:
+                case TRAFFIC_PERMISSION_UNINSTALLED:
                     uninstalledAppIds.add(netdPermissionsAppIds.keyAt(i));
                     break;
                 default:
@@ -1188,15 +1244,15 @@
             // TODO: add a lock inside netd to protect IPC trafficSetNetPermForUids()
             if (allPermissionAppIds.size() != 0) {
                 mBpfNetMaps.setNetPermForUids(
-                        PERMISSION_INTERNET | PERMISSION_UPDATE_DEVICE_STATS,
+                        TRAFFIC_PERMISSION_INTERNET | TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS,
                         toIntArray(allPermissionAppIds));
             }
             if (internetPermissionAppIds.size() != 0) {
-                mBpfNetMaps.setNetPermForUids(PERMISSION_INTERNET,
+                mBpfNetMaps.setNetPermForUids(TRAFFIC_PERMISSION_INTERNET,
                         toIntArray(internetPermissionAppIds));
             }
             if (updateStatsPermissionAppIds.size() != 0) {
-                mBpfNetMaps.setNetPermForUids(PERMISSION_UPDATE_DEVICE_STATS,
+                mBpfNetMaps.setNetPermForUids(TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS,
                         toIntArray(updateStatsPermissionAppIds));
             }
             if (noPermissionAppIds.size() != 0) {
@@ -1204,7 +1260,7 @@
                         toIntArray(noPermissionAppIds));
             }
             if (uninstalledAppIds.size() != 0) {
-                mBpfNetMaps.setNetPermForUids(PERMISSION_UNINSTALLED,
+                mBpfNetMaps.setNetPermForUids(TRAFFIC_PERMISSION_UNINSTALLED,
                         toIntArray(uninstalledAppIds));
             }
         } catch (RemoteException | ServiceSpecificException e) {
@@ -1311,4 +1367,11 @@
     private static void loge(String s, Throwable e) {
         Log.e(TAG, s, e);
     }
+
+    private class PermissionChangeListener implements PackageManager.OnPermissionsChangedListener {
+        @Override
+        public void onPermissionsChanged(int uid) {
+            setLocalNetworkPermissions(uid, null);
+        }
+    }
 }
diff --git a/service/src/com/android/server/net/HeaderCompressionUtils.java b/service/src/com/android/server/net/HeaderCompressionUtils.java
new file mode 100644
index 0000000..5bd3a76
--- /dev/null
+++ b/service/src/com/android/server/net/HeaderCompressionUtils.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import android.util.Log;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+public class HeaderCompressionUtils {
+    private static final String TAG = "L2capHeaderCompressionUtils";
+    private static final int IPV6_HEADER_SIZE = 40;
+
+    private static byte[] decodeIpv6Address(ByteBuffer buffer, int mode, boolean isMulticast)
+            throws BufferUnderflowException, IOException {
+        // Mode is equivalent between SAM and DAM; however, isMulticast only applies to DAM.
+        final byte[] address = new byte[16];
+        // If multicast bit is set, mix it in the mode, so that the lower two bits represent the
+        // address mode, and the upper bit represents multicast compression.
+        switch ((isMulticast ? 0b100 : 0) | mode) {
+            case 0b000: // 128 bits. The full address is carried in-line.
+            case 0b100:
+                buffer.get(address);
+                break;
+            case 0b001: // 64 bits. The first 64-bits of the fe80:: address are elided.
+                address[0] = (byte) 0xfe;
+                address[1] = (byte) 0x80;
+                buffer.get(address, 8 /*off*/, 8 /*len*/);
+                break;
+            case 0b010: // 16 bits. fe80::ff:fe00:XXXX, where XXXX are the bits carried in-line
+                address[0] = (byte) 0xfe;
+                address[1] = (byte) 0x80;
+                address[11] = (byte) 0xff;
+                address[12] = (byte) 0xfe;
+                buffer.get(address, 14 /*off*/, 2 /*len*/);
+                break;
+            case 0b011: // 0 bits. The address is fully elided and derived from BLE MAC address
+                // Note that on Android, the BLE MAC addresses are not exposed via the API;
+                // therefore, this compression mode cannot be supported.
+                throw new IOException("Address cannot be fully elided");
+            case 0b101: // 48 bits. The address takes the form ffXX::00XX:XXXX:XXXX.
+                address[0] = (byte) 0xff;
+                address[1] = buffer.get();
+                buffer.get(address, 11 /*off*/, 5 /*len*/);
+                break;
+            case 0b110: // 32 bits. The address takes the form ffXX::00XX:XXXX
+                address[0] = (byte) 0xff;
+                address[1] = buffer.get();
+                buffer.get(address, 13 /*off*/, 3 /*len*/);
+                break;
+            case 0b111: // 8 bits. The address takes the form ff02::00XX.
+                address[0] = (byte) 0xff;
+                address[1] = (byte) 0x02;
+                address[15] = buffer.get();
+                break;
+        }
+        return address;
+    }
+
+    /**
+     * Performs 6lowpan header decompression in place.
+     *
+     * Note that the passed in buffer must have enough capacity for successful decompression.
+     *
+     * @param bytes The buffer containing the packet.
+     * @param len The size of the packet
+     * @return decompressed size or zero
+     * @throws BufferUnderflowException if an illegal packet is encountered.
+     * @throws IOException if an unsupported option is encountered.
+     */
+    public static int decompress6lowpan(byte[] bytes, int len)
+            throws BufferUnderflowException, IOException {
+        // Note that ByteBuffer's default byte order is big endian.
+        final ByteBuffer inBuffer = ByteBuffer.wrap(bytes);
+        inBuffer.limit(len);
+
+        // LOWPAN_IPHC base encoding:
+        //   0   1   2   3   4   5   6   7 | 8   9  10  11  12  13  14  15
+        // +---+---+---+---+---+---+---+---|---+---+---+---+---+---+---+---+
+        // | 0 | 1 | 1 |  TF   |NH | HLIM  |CID|SAC|  SAM  | M |DAC|  DAM  |
+        // +---+---+---+---+---+---+---+---|---+---+---+---+---+---+---+---+
+        final int iphc1 = inBuffer.get() & 0xff;
+        final int iphc2 = inBuffer.get() & 0xff;
+        // Dispatch must start with 0b011.
+        if ((iphc1 & 0xe0) != 0x60) {
+            throw new IOException("LOWPAN_IPHC does not start with 011");
+        }
+
+        final int tf = (iphc1 >> 3) & 3;         // Traffic class
+        final boolean nh = (iphc1 & 4) != 0;     // Next header
+        final int hlim = iphc1 & 3;              // Hop limit
+        final boolean cid = (iphc2 & 0x80) != 0; // Context identifier extension
+        final boolean sac = (iphc2 & 0x40) != 0; // Source address compression
+        final int sam = (iphc2 >> 4) & 3;        // Source address mode
+        final boolean m = (iphc2 & 8) != 0;      // Multicast compression
+        final boolean dac = (iphc2 & 4) != 0;    // Destination address compression
+        final int dam = iphc2 & 3;               // Destination address mode
+
+        final ByteBuffer ipv6Header = ByteBuffer.allocate(IPV6_HEADER_SIZE);
+
+        final int trafficClass;
+        final int flowLabel;
+        switch (tf) {
+            case 0b00: // ECN + DSCP + 4-bit Pad + Flow Label (4 bytes)
+                trafficClass = inBuffer.get() & 0xff;
+                flowLabel = (inBuffer.get() & 0x0f) << 16
+                        | (inBuffer.get() & 0xff) << 8
+                        | (inBuffer.get() & 0xff);
+                break;
+            case 0b01: // ECN + 2-bit Pad + Flow Label (3 bytes), DSCP is elided.
+                final int firstByte = inBuffer.get() & 0xff;
+                //     0     1     2     3     4     5     6     7
+                // +-----+-----+-----+-----+-----+-----+-----+-----+
+                // |          DS FIELD, DSCP           | ECN FIELD |
+                // +-----+-----+-----+-----+-----+-----+-----+-----+
+                // rfc6282 does not explicitly state what value to use for DSCP, assuming 0.
+                trafficClass = firstByte >> 6;
+                flowLabel = (firstByte & 0x0f) << 16
+                        | (inBuffer.get() & 0xff) << 8
+                        | (inBuffer.get() & 0xff);
+                break;
+            case 0b10: // ECN + DSCP (1 byte), Flow Label is elided.
+                trafficClass = inBuffer.get() & 0xff;
+                // rfc6282 does not explicitly state what value to use, assuming 0.
+                flowLabel = 0;
+                break;
+            case 0b11: // Traffic Class and Flow Label are elided.
+                // rfc6282 does not explicitly state what value to use, assuming 0.
+                trafficClass = 0;
+                flowLabel = 0;
+                break;
+            default:
+                // This cannot happen. Crash if it does.
+                throw new IllegalStateException("Illegal TF value");
+        }
+
+        // Write version, traffic class, and flow label
+        final int versionTcFlowLabel = (6 << 28) | (trafficClass << 20) | flowLabel;
+        ipv6Header.putInt(versionTcFlowLabel);
+
+        // Payload length is still unknown. Use 0 for now.
+        ipv6Header.putShort((short) 0);
+
+        // We do not use UDP or extension header compression, therefore the next header
+        // cannot be compressed.
+        if (nh) throw new IOException("Next header cannot be compressed");
+        // Write next header
+        ipv6Header.put(inBuffer.get());
+
+        final byte hopLimit;
+        switch (hlim) {
+            case 0b00: // The Hop Limit field is carried in-line.
+                hopLimit = inBuffer.get();
+                break;
+            case 0b01: // The Hop Limit field is compressed and the hop limit is 1.
+                hopLimit = 1;
+                break;
+            case 0b10: // The Hop Limit field is compressed and the hop limit is 64.
+                hopLimit = 64;
+                break;
+            case 0b11: // The Hop Limit field is compressed and the hop limit is 255.
+                hopLimit = (byte) 255;
+                break;
+            default:
+                // This cannot happen. Crash if it does.
+                throw new IllegalStateException("Illegal HLIM value");
+        }
+        ipv6Header.put(hopLimit);
+
+        if (cid) throw new IOException("Context based compression not supported");
+        if (sac) throw new IOException("Context based compression not supported");
+        if (dac) throw new IOException("Context based compression not supported");
+
+        // Write source address
+        ipv6Header.put(decodeIpv6Address(inBuffer, sam, false /* isMulticast */));
+
+        // Write destination address
+        ipv6Header.put(decodeIpv6Address(inBuffer, dam, m));
+
+        // Go back and fix up payloadLength
+        final short payloadLength = (short) inBuffer.remaining();
+        ipv6Header.putShort(4, payloadLength);
+
+        // Done! Check that 40 bytes were written.
+        if (ipv6Header.position() != IPV6_HEADER_SIZE) {
+            // This indicates a bug in our code -> crash.
+            throw new IllegalStateException("Faulty decompression wrote less than 40 bytes");
+        }
+
+        // Ensure there is enough room in the buffer
+        final int packetLength = payloadLength + IPV6_HEADER_SIZE;
+        if (bytes.length < packetLength) {
+            throw new IOException("Decompressed packet exceeds buffer size");
+        }
+
+        // Move payload bytes back to make room for the header
+        inBuffer.limit(packetLength);
+        System.arraycopy(bytes, inBuffer.position(), bytes, IPV6_HEADER_SIZE, payloadLength);
+        // Copy IPv6 header to the beginning of the buffer.
+        inBuffer.position(0);
+        ipv6Header.flip();
+        inBuffer.put(ipv6Header);
+
+        return packetLength;
+    }
+
+    /**
+     * Performs 6lowpan header compression in place.
+     *
+     * @param bytes The buffer containing the packet.
+     * @param len The size of the packet
+     * @return compressed size or zero
+     * @throws BufferUnderflowException if an illegal packet is encountered.
+     * @throws IOException if an unsupported option is encountered.
+     */
+    public static int compress6lowpan(byte[] bytes, final int len)
+            throws BufferUnderflowException, IOException {
+        // Compression only happens on egress, i.e. the packet is read from the tun fd.
+        // This means that this code can be a bit more lenient.
+        if (len < 40) {
+            Log.wtf(TAG, "Encountered short (<40 byte) packet");
+            return 0;
+        }
+
+        // Note that ByteBuffer's default byte order is big endian.
+        final ByteBuffer inBuffer = ByteBuffer.wrap(bytes);
+        inBuffer.limit(len);
+
+        // Check that the packet is an IPv6 packet
+        final int versionTcFlowLabel = inBuffer.getInt() & 0xffffffff;
+        if ((versionTcFlowLabel >> 28) != 6) {
+            return 0;
+        }
+
+        // Check that the payload length matches the packet length - 40.
+        int payloadLength = inBuffer.getShort();
+        if (payloadLength != len - IPV6_HEADER_SIZE) {
+            throw new IOException("Encountered packet with payload length mismatch");
+        }
+
+        // Implements rfc 6282 6lowpan header compression using iphc 0110 0000 0000 0000 (all
+        // fields are carried inline).
+        inBuffer.position(0);
+        inBuffer.put((byte) 0x60);
+        inBuffer.put((byte) 0x00);
+        final byte trafficClass = (byte) ((versionTcFlowLabel >> 20) & 0xff);
+        inBuffer.put(trafficClass);
+        final byte flowLabelMsb = (byte) ((versionTcFlowLabel >> 16) & 0x0f);
+        final short flowLabelLsb = (short) (versionTcFlowLabel & 0xffff);
+        inBuffer.put(flowLabelMsb);
+        // Note: the next putShort overrides the payload length. This is WAI as the payload length
+        // is reconstructed via L2CAP packet length.
+        inBuffer.putShort(flowLabelLsb);
+
+        // Since the iphc (2 bytes) matches the payload length that was elided (2 bytes), the length
+        // of the packet did not change.
+        return len;
+    }
+}
diff --git a/service/src/com/android/server/net/L2capNetwork.java b/service/src/com/android/server/net/L2capNetwork.java
new file mode 100644
index 0000000..ca155db
--- /dev/null
+++ b/service/src/com/android/server/net/L2capNetwork.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import static android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_6LOWPAN;
+
+import android.annotation.Nullable;
+import android.bluetooth.BluetoothSocket;
+import android.content.Context;
+import android.net.L2capNetworkSpecifier;
+import android.net.LinkProperties;
+import android.net.NetworkAgent;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkProvider;
+import android.net.NetworkScore;
+import android.net.ip.IIpClient;
+import android.net.ip.IpClientCallbacks;
+import android.net.ip.IpClientManager;
+import android.net.ip.IpClientUtil;
+import android.net.shared.ProvisioningConfiguration;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import com.android.server.L2capNetworkProvider;
+
+public class L2capNetwork {
+    private static final NetworkScore NETWORK_SCORE = new NetworkScore.Builder().build();
+    private final String mLogTag;
+    private final Handler mHandler;
+    private final L2capPacketForwarder mForwarder;
+    private final NetworkCapabilities mNetworkCapabilities;
+    private final NetworkAgent mNetworkAgent;
+
+    /** IpClient wrapper to handle IPv6 link-local provisioning for L2CAP tun.
+     *
+     * Note that the IpClient does not need to be stopped.
+     */
+    public static class L2capIpClient extends IpClientCallbacks {
+        private final String mLogTag;
+        private final ConditionVariable mOnIpClientCreatedCv = new ConditionVariable(false);
+        private final ConditionVariable mOnProvisioningSuccessCv = new ConditionVariable(false);
+        @Nullable
+        private IpClientManager mIpClient;
+        @Nullable
+        private volatile LinkProperties mLinkProperties;
+
+        public L2capIpClient(String logTag, Context context, String ifname) {
+            mLogTag = logTag;
+            IpClientUtil.makeIpClient(context, ifname, this);
+        }
+
+        @Override
+        public void onIpClientCreated(IIpClient ipClient) {
+            mIpClient = new IpClientManager(ipClient, mLogTag);
+            mOnIpClientCreatedCv.open();
+        }
+
+        @Override
+        public void onProvisioningSuccess(LinkProperties lp) {
+            Log.d(mLogTag, "Successfully provisioned l2cap tun: " + lp);
+            mLinkProperties = lp;
+            mOnProvisioningSuccessCv.open();
+        }
+
+        @Override
+        public void onProvisioningFailure(LinkProperties lp) {
+            Log.i(mLogTag, "Failed to provision l2cap tun: " + lp);
+            mLinkProperties = null;
+            mOnProvisioningSuccessCv.open();
+        }
+
+        /**
+         * Starts IPv6 link-local provisioning.
+         *
+         * @return LinkProperties on success, null on failure.
+         */
+        @Nullable
+        public LinkProperties start() {
+            mOnIpClientCreatedCv.block();
+            // mIpClient guaranteed non-null.
+            final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                    .withoutIPv4()
+                    .withIpv6LinkLocalOnly()
+                    .withRandomMacAddress() // addr_gen_mode EUI64 -> random on tun.
+                    .build();
+            mIpClient.startProvisioning(config);
+            // "Provisioning" is guaranteed to succeed as link-local only mode does not actually
+            // require any provisioning.
+            mOnProvisioningSuccessCv.block();
+            return mLinkProperties;
+        }
+    }
+
+    public interface ICallback {
+        /** Called when an error is encountered */
+        void onError(L2capNetwork network);
+        /** Called when CS triggers NetworkAgent#onNetworkUnwanted */
+        void onNetworkUnwanted(L2capNetwork network);
+    }
+
+    public L2capNetwork(String logTag, Handler handler, Context context, NetworkProvider provider,
+            BluetoothSocket socket, ParcelFileDescriptor tunFd, NetworkCapabilities nc,
+            LinkProperties lp, L2capNetworkProvider.Dependencies deps, ICallback cb) {
+        mLogTag = logTag;
+        mHandler = handler;
+        mNetworkCapabilities = nc;
+
+        final L2capNetworkSpecifier spec = (L2capNetworkSpecifier) nc.getNetworkSpecifier();
+        final boolean compressHeaders = spec.getHeaderCompression() == HEADER_COMPRESSION_6LOWPAN;
+
+        mForwarder = deps.createL2capPacketForwarder(handler, tunFd, socket, compressHeaders,
+                () -> {
+            // TODO: add a check that this callback is invoked on the handler thread.
+            cb.onError(L2capNetwork.this);
+        });
+
+        final NetworkAgentConfig config = new NetworkAgentConfig.Builder().build();
+        mNetworkAgent = new NetworkAgent(context, mHandler.getLooper(), mLogTag,
+                nc, lp, NETWORK_SCORE, config, provider) {
+            @Override
+            public void onNetworkUnwanted() {
+                Log.i(mLogTag, "Network is unwanted");
+                // TODO: add a check that this callback is invoked on the handler thread.
+                cb.onNetworkUnwanted(L2capNetwork.this);
+            }
+        };
+        mNetworkAgent.register();
+        mNetworkAgent.markConnected();
+    }
+
+    /** Create an L2capNetwork or return null on failure. */
+    @Nullable
+    public static L2capNetwork create(Handler handler, Context context, NetworkProvider provider,
+            String ifname, BluetoothSocket socket, ParcelFileDescriptor tunFd,
+            NetworkCapabilities nc, L2capNetworkProvider.Dependencies deps, ICallback cb) {
+        // TODO: add a check that this function is invoked on the handler thread.
+        final String logTag = String.format("L2capNetwork[%s]", ifname);
+
+        // L2capIpClient#start() blocks until provisioning either succeeds (and returns
+        // LinkProperties) or fails (and returns null).
+        // Note that since L2capNetwork is using IPv6 link-local provisioning the most likely
+        // (only?) failure mode is due to the interface disappearing.
+        final LinkProperties lp = deps.createL2capIpClient(logTag, context, ifname).start();
+        if (lp == null) return null;
+
+        return new L2capNetwork(
+                logTag, handler, context, provider, socket, tunFd, nc, lp, deps, cb);
+    }
+
+    /** Get the NetworkCapabilities used for this Network */
+    public NetworkCapabilities getNetworkCapabilities() {
+        return mNetworkCapabilities;
+    }
+
+    /** Tear down the network and associated resources */
+    public void tearDown() {
+        mNetworkAgent.unregister();
+        mForwarder.tearDown();
+    }
+}
diff --git a/service/src/com/android/server/net/L2capPacketForwarder.java b/service/src/com/android/server/net/L2capPacketForwarder.java
new file mode 100644
index 0000000..8420d60
--- /dev/null
+++ b/service/src/com/android/server/net/L2capPacketForwarder.java
@@ -0,0 +1,341 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import static com.android.server.net.HeaderCompressionUtils.compress6lowpan;
+import static com.android.server.net.HeaderCompressionUtils.decompress6lowpan;
+
+import android.bluetooth.BluetoothSocket;
+import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.BufferUnderflowException;
+
+/**
+ * Forwards packets from a BluetoothSocket of type L2CAP to a tun fd and vice versa.
+ *
+ * The forwarding logic operates on raw IP packets and there are no ethernet headers.
+ * Therefore, L3 MTU = L2 MTU.
+ */
+public class L2capPacketForwarder {
+    private static final String TAG = "L2capPacketForwarder";
+
+    // DCT specifies an MTU of 1500.
+    // TODO: Set /proc/sys/net/ipv6/conf/${iface}/mtu to 1280 and the link MTU to 1528 to accept
+    // slightly larger packets on ingress (i.e. packets passing through a NAT64 gateway).
+    // MTU determines the value of the read buffers, so use the larger of the two.
+    @VisibleForTesting
+    public static final int MTU = 1528;
+    private final Handler mHandler;
+    private final IReadWriteFd mTunFd;
+    private final IReadWriteFd mL2capFd;
+    private final L2capThread mIngressThread;
+    private final L2capThread mEgressThread;
+    private final ICallback mCallback;
+
+    public interface ICallback {
+        /** Called when an error is encountered; should tear down forwarding. */
+        void onError();
+    }
+
+    private interface IReadWriteFd {
+        /**
+         * Read up to len bytes into bytes[off] and return bytes actually read.
+         *
+         * bytes[] must be of size >= off + len.
+         */
+        int read(byte[] bytes, int off, int len) throws IOException;
+        /**
+         * Write len bytes starting from bytes[off]
+         *
+         * bytes[] must be of size >= off + len.
+         */
+        void write(byte[] bytes, int off, int len) throws IOException;
+        /** Disallow further receptions, shutdown(fd, SHUT_RD) */
+        void shutdownRead();
+        /** Disallow further transmissions, shutdown(fd, SHUT_WR) */
+        void shutdownWrite();
+        /** Close the fd */
+        void close();
+    }
+
+    @VisibleForTesting
+    public static class BluetoothSocketWrapper implements IReadWriteFd {
+        private final BluetoothSocket mSocket;
+        private final InputStream mInputStream;
+        private final OutputStream mOutputStream;
+
+        public BluetoothSocketWrapper(BluetoothSocket socket) {
+            // TODO: assert that MTU can fit within Bluetooth L2CAP SDU (maximum size of an L2CAP
+            // packet). The L2CAP SDU is 65535 by default, but can be less when using hardware
+            // offload.
+            mSocket = socket;
+            try {
+                mInputStream = socket.getInputStream();
+                mOutputStream = socket.getOutputStream();
+            } catch (IOException e) {
+                // Per the API docs, this should not actually be possible.
+                Log.wtf(TAG, "Failed to get Input/OutputStream", e);
+                // Fail hard.
+                throw new IllegalStateException("Failed to get Input/OutputStream");
+            }
+        }
+
+        /** Read from the BluetoothSocket. */
+        @Override
+        public int read(byte[] bytes, int off, int len) throws IOException {
+            // Note: EINTR is handled internally and automatically triggers a retry loop.
+            int bytesRead = mInputStream.read(bytes, off, len);
+            if (bytesRead < 0 || bytesRead > MTU) {
+                // Don't try to recover, just trigger network teardown. This might indicate a bug in
+                // the Bluetooth stack.
+                throw new IOException("Packet exceeds MTU or reached EOF. Read: " + bytesRead);
+            }
+            return bytesRead;
+        }
+
+        /** Write to the BluetoothSocket. */
+        @Override
+        public void write(byte[] bytes, int off, int len) throws IOException {
+            // Note: EINTR is handled internally and automatically triggers a retry loop.
+            mOutputStream.write(bytes, off, len);
+        }
+
+        @Override
+        public void shutdownRead() {
+            // BluetoothSocket does not expose methods to shutdown read / write individually;
+            // however, BluetoothSocket#close() shuts down both read and write and is safe to be
+            // called multiple times from any thread.
+            try {
+                mSocket.close();
+            } catch (IOException e) {
+                Log.w(TAG, "shutdownRead: Failed to close BluetoothSocket", e);
+            }
+        }
+
+        @Override
+        public void shutdownWrite() {
+            // BluetoothSocket does not expose methods to shutdown read / write individually;
+            // however, BluetoothSocket#close() shuts down both read and write and is safe to be
+            // called multiple times from any thread.
+            try {
+                mSocket.close();
+            } catch (IOException e) {
+                Log.w(TAG, "shutdownWrite: Failed to close BluetoothSocket", e);
+            }
+        }
+
+        @Override
+        public void close() {
+            // BluetoothSocket#close() is safe to be called multiple times.
+            try {
+                mSocket.close();
+            } catch (IOException e) {
+                Log.w(TAG, "close: Failed to close BluetoothSocket", e);
+            }
+        }
+    }
+
+    @VisibleForTesting
+    public static class FdWrapper implements IReadWriteFd {
+        private final ParcelFileDescriptor mFd;
+
+        public FdWrapper(ParcelFileDescriptor fd) {
+            mFd = fd;
+        }
+
+        @Override
+        public int read(byte[] bytes, int off, int len) throws IOException {
+            try {
+                // Note: EINTR is handled internally and automatically triggers a retry loop.
+                return Os.read(mFd.getFileDescriptor(), bytes, off, len);
+            } catch (ErrnoException e) {
+                throw new IOException(e);
+            }
+        }
+
+        /**
+         * Write to BluetoothSocket.
+         */
+        @Override
+        public void write(byte[] bytes, int off, int len) throws IOException {
+            try {
+                // Note: EINTR is handled internally and automatically triggers a retry loop.
+                Os.write(mFd.getFileDescriptor(), bytes, off, len);
+            } catch (ErrnoException e) {
+                throw new IOException(e);
+            }
+        }
+
+        @Override
+        public void shutdownRead() {
+            try {
+                Os.shutdown(mFd.getFileDescriptor(), OsConstants.SHUT_RD);
+            } catch (ErrnoException e) {
+                Log.w(TAG, "shutdownRead: Failed to shutdown(fd, SHUT_RD)", e);
+            }
+        }
+
+        @Override
+        public void shutdownWrite() {
+            try {
+                Os.shutdown(mFd.getFileDescriptor(), OsConstants.SHUT_WR);
+            } catch (ErrnoException e) {
+                Log.w(TAG, "shutdownWrite: Failed to shutdown(fd, SHUT_WR)", e);
+            }
+        }
+
+        @Override
+        public void close() {
+            try {
+                // Safe to call multiple times. Both Os.close(FileDescriptor) and
+                // ParcelFileDescriptor#close() offer protection against double-closing an fd.
+                mFd.close();
+            } catch (IOException e) {
+                Log.w(TAG, "close: Failed to close fd", e);
+            }
+        }
+    }
+
+    private class L2capThread extends Thread {
+        // Set mBuffer length to MTU + 1 to catch read() overflows.
+        private final byte[] mBuffer = new byte[MTU + 1];
+        private volatile boolean mIsRunning = true;
+
+        private final String mLogTag;
+        private final IReadWriteFd mReadFd;
+        private final IReadWriteFd mWriteFd;
+        private final boolean mIsIngress;
+        private final boolean mCompressHeaders;
+
+        L2capThread(IReadWriteFd readFd, IReadWriteFd writeFd, boolean isIngress,
+                boolean compressHeaders) {
+            super("L2capNetworkProvider-ForwarderThread");
+            mLogTag = isIngress ? "L2capForwarderThread-Ingress" : "L2capForwarderThread-Egress";
+            mReadFd = readFd;
+            mWriteFd = writeFd;
+            mIsIngress = isIngress;
+            mCompressHeaders = compressHeaders;
+        }
+
+        private void postOnError() {
+            mHandler.post(() -> {
+                // All callbacks must be called on handler thread.
+                mCallback.onError();
+            });
+        }
+
+        @Override
+        public void run() {
+            while (mIsRunning) {
+                try {
+                    int readBytes = mReadFd.read(mBuffer, 0 /*off*/, mBuffer.length);
+                    // No bytes to write, continue.
+                    if (readBytes <= 0) {
+                        Log.w(mLogTag, "Zero-byte read encountered: " + readBytes);
+                        continue;
+                    }
+
+                    if (mCompressHeaders) {
+                        if (mIsIngress) {
+                            readBytes = decompress6lowpan(mBuffer, readBytes);
+                        } else {
+                            readBytes = compress6lowpan(mBuffer, readBytes);
+                        }
+                    }
+
+                    // If the packet is 0-length post de/compression or exceeds MTU, drop it.
+                    // Note that a large read on BluetoothSocket throws an IOException to tear down
+                    // the network.
+                    if (readBytes <= 0 || readBytes > MTU) continue;
+
+                    mWriteFd.write(mBuffer, 0 /*off*/, readBytes);
+                } catch (IOException|BufferUnderflowException e) {
+                    Log.e(mLogTag, "L2capThread exception", e);
+                    // Tear down the network on any error.
+                    mIsRunning = false;
+                    // Notify provider that forwarding has stopped.
+                    postOnError();
+                }
+            }
+        }
+
+        public void tearDown() {
+            mIsRunning = false;
+            mReadFd.shutdownRead();
+            mWriteFd.shutdownWrite();
+        }
+    }
+
+    public L2capPacketForwarder(Handler handler, ParcelFileDescriptor tunFd, BluetoothSocket socket,
+            boolean compressHdrs, ICallback cb) {
+        this(handler, new FdWrapper(tunFd), new BluetoothSocketWrapper(socket), compressHdrs, cb);
+    }
+
+    @VisibleForTesting
+    L2capPacketForwarder(Handler handler, IReadWriteFd tunFd, IReadWriteFd l2capFd,
+            boolean compressHeaders, ICallback cb) {
+        mHandler = handler;
+        mTunFd = tunFd;
+        mL2capFd = l2capFd;
+        mCallback = cb;
+
+        mIngressThread = new L2capThread(l2capFd, tunFd, true /*isIngress*/, compressHeaders);
+        mEgressThread = new L2capThread(tunFd, l2capFd, false /*isIngress*/, compressHeaders);
+
+        mIngressThread.start();
+        mEgressThread.start();
+    }
+
+    /**
+     * Tear down the L2capPacketForwarder.
+     *
+     * This operation closes both the passed tun fd and BluetoothSocket.
+     **/
+    public void tearDown() {
+        // Destroying both threads first guarantees that both read and write side of FD have been
+        // shutdown.
+        mIngressThread.tearDown();
+        mEgressThread.tearDown();
+
+        // In order to interrupt a blocking read on the BluetoothSocket, the BluetoothSocket must be
+        // closed (which triggers shutdown()). This means, the BluetoothSocket must be closed inside
+        // L2capPacketForwarder. Tear down the tun fd alongside it for consistency.
+        mTunFd.close();
+        mL2capFd.close();
+
+        try {
+            mIngressThread.join();
+        } catch (InterruptedException e) {
+            // join() interrupted in tearDown path, do nothing.
+        }
+        try {
+            mEgressThread.join();
+        } catch (InterruptedException e) {
+            // join() interrupted in tearDown path, do nothing.
+        }
+    }
+}
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index b4a3b8a..0eab6e7 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -350,7 +350,7 @@
     // TODO: remove "apex_available:platform".
     apex_available: [
         "//apex_available:platform",
-        "com.android.btservices",
+        "com.android.bt",
         "com.android.tethering",
         "com.android.wifi",
     ],
diff --git a/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java b/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
index 553a24b..8b2fe58 100644
--- a/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
+++ b/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
@@ -161,9 +161,9 @@
         netd.tetherInterfaceAdd(iface);
         networkAddInterface(netd, netId, iface, maxAttempts, pollingIntervalMs);
         // Activate a route to dest and IPv6 link local.
-        modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID,
+        modifyRoute(netd, ModifyOperation.ADD, netId,
                 new RouteInfo(dest, null, iface, RTN_UNICAST));
-        modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID,
+        modifyRoute(netd, ModifyOperation.ADD, netId,
                 new RouteInfo(new IpPrefix("fe80::/64"), null, iface, RTN_UNICAST));
     }
 
@@ -194,12 +194,12 @@
     }
 
     /** Reset interface for tethering. */
-    public static void untetherInterface(final INetd netd, String iface)
+    public static void untetherInterface(final INetd netd, int netId, String iface)
             throws RemoteException, ServiceSpecificException {
         try {
             netd.tetherInterfaceRemove(iface);
         } finally {
-            netd.networkRemoveInterface(INetd.LOCAL_NET_ID, iface);
+            netd.networkRemoveInterface(netId, iface);
         }
     }
 
diff --git a/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java b/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java
index ab90a50..c2fbb56 100644
--- a/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java
+++ b/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java
@@ -16,7 +16,6 @@
 
 package com.android.net.module.util;
 
-import static android.net.INetd.LOCAL_NET_ID;
 import static android.system.OsConstants.EBUSY;
 
 import static com.android.testutils.MiscAsserts.assertThrows;
@@ -63,6 +62,7 @@
 
     private static final String IFACE = "TEST_IFACE";
     private static final IpPrefix TEST_IPPREFIX = new IpPrefix("192.168.42.1/24");
+    private static final int TEST_NET_ID = 123;
 
     @Before
     public void setUp() throws Exception {
@@ -134,7 +134,7 @@
             }
 
             throw new ServiceSpecificException(EBUSY);
-        }).when(mNetd).networkAddInterface(LOCAL_NET_ID, IFACE);
+        }).when(mNetd).networkAddInterface(TEST_NET_ID, IFACE);
     }
 
     class Counter {
@@ -163,7 +163,7 @@
         setNetworkAddInterfaceOutcome(new ServiceSpecificException(expectedCode), expectedTries);
 
         try {
-            NetdUtils.tetherInterface(mNetd, LOCAL_NET_ID, IFACE, TEST_IPPREFIX, 20, 0);
+            NetdUtils.tetherInterface(mNetd, TEST_NET_ID, IFACE, TEST_IPPREFIX, 20, 0);
             fail("Expect throw ServiceSpecificException");
         } catch (ServiceSpecificException e) {
             assertEquals(e.errorCode, expectedCode);
@@ -177,7 +177,7 @@
         setNetworkAddInterfaceOutcome(new RemoteException(), expectedTries);
 
         try {
-            NetdUtils.tetherInterface(mNetd, LOCAL_NET_ID, IFACE, TEST_IPPREFIX, 20, 0);
+            NetdUtils.tetherInterface(mNetd, TEST_NET_ID, IFACE, TEST_IPPREFIX, 20, 0);
             fail("Expect throw RemoteException");
         } catch (RemoteException e) { }
 
@@ -187,18 +187,19 @@
 
     private void verifyNetworkAddInterfaceFails(int expectedTries) throws Exception {
         verify(mNetd).tetherInterfaceAdd(IFACE);
-        verify(mNetd, times(expectedTries)).networkAddInterface(LOCAL_NET_ID, IFACE);
+        verify(mNetd, times(expectedTries)).networkAddInterface(TEST_NET_ID, IFACE);
         verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), any(), any());
+
         verifyNoMoreInteractions(mNetd);
     }
 
     private void verifyTetherInterfaceSucceeds(int expectedTries) throws Exception {
         setNetworkAddInterfaceOutcome(null, expectedTries);
 
-        NetdUtils.tetherInterface(mNetd, LOCAL_NET_ID, IFACE, TEST_IPPREFIX);
+        NetdUtils.tetherInterface(mNetd, TEST_NET_ID, IFACE, TEST_IPPREFIX);
         verify(mNetd).tetherInterfaceAdd(IFACE);
-        verify(mNetd, times(expectedTries)).networkAddInterface(LOCAL_NET_ID, IFACE);
-        verify(mNetd, times(2)).networkAddRoute(eq(LOCAL_NET_ID), eq(IFACE), any(), any());
+        verify(mNetd, times(expectedTries)).networkAddInterface(TEST_NET_ID, IFACE);
+        verify(mNetd, times(2)).networkAddRoute(eq(TEST_NET_ID), eq(IFACE), any(), any());
         verifyNoMoreInteractions(mNetd);
         reset(mNetd);
     }
diff --git a/staticlibs/device/com/android/net/module/util/RealtimeScheduler.java b/staticlibs/device/com/android/net/module/util/RealtimeScheduler.java
new file mode 100644
index 0000000..c8fdf72
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/RealtimeScheduler.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR;
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.MessageQueue;
+import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.CloseGuard;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import java.io.IOException;
+import java.util.PriorityQueue;
+
+/**
+ * Represents a realtime scheduler object used for scheduling tasks with precise delays.
+ * Compared to {@link Handler#postDelayed}, this class offers enhanced accuracy for delayed
+ * callbacks by accounting for periods when the device is in deep sleep.
+ *
+ *  <p> This class is designed for use exclusively from the handler thread.
+ *
+ * **Usage Examples:**
+ *
+ * ** Scheduling recurring tasks with the same RealtimeScheduler **
+ *
+ * ```java
+ * // Create a RealtimeScheduler
+ * final RealtimeScheduler scheduler = new RealtimeScheduler(handler);
+ *
+ * // Schedule a new task with a delay.
+ * scheduler.postDelayed(() -> taskToExecute(), delayTime);
+ *
+ * // Once the delay has elapsed, and the task is running, schedule another task.
+ * scheduler.postDelayed(() -> anotherTaskToExecute(), anotherDelayTime);
+ *
+ * // Remember to close the RealtimeScheduler after all tasks have finished running.
+ * scheduler.close();
+ * ```
+ */
+public class RealtimeScheduler {
+    private static final String TAG = RealtimeScheduler.class.getSimpleName();
+    // EVENT_ERROR may be generated even if not specified, as per its javadoc.
+    private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR;
+    private final CloseGuard mGuard = new CloseGuard();
+    @NonNull
+    private final Handler mHandler;
+    @NonNull
+    private final MessageQueue mQueue;
+    @NonNull
+    private final ParcelFileDescriptor mParcelFileDescriptor;
+    private final int mFdInt;
+
+    private final PriorityQueue<Task> mTaskQueue;
+
+    /**
+     * An abstract class for defining tasks that can be executed using a {@link Handler}.
+     */
+    private abstract static class Task implements Comparable<Task> {
+        private final long mRunTimeMs;
+        private final long mCreatedTimeNs = SystemClock.elapsedRealtimeNanos();
+
+        /**
+         * create a task with a run time
+         */
+        Task(long runTimeMs) {
+            mRunTimeMs = runTimeMs;
+        }
+
+        /**
+         * Executes the task using the provided {@link Handler}.
+         *
+         * @param handler The {@link Handler} to use for executing the task.
+         */
+        abstract void post(Handler handler);
+
+        @Override
+        public int compareTo(@NonNull Task o) {
+            if (mRunTimeMs != o.mRunTimeMs) {
+                return Long.compare(mRunTimeMs, o.mRunTimeMs);
+            }
+            return Long.compare(mCreatedTimeNs, o.mCreatedTimeNs);
+        }
+
+        /**
+         * Returns the run time of the task.
+         */
+        public long getRunTimeMs() {
+            return mRunTimeMs;
+        }
+    }
+
+    /**
+     * A task that sends a {@link Message} using a {@link Handler}.
+     */
+    private static class MessageTask extends Task {
+        private final Message mMessage;
+
+        MessageTask(Message message, long runTimeMs) {
+            super(runTimeMs);
+            mMessage = message;
+        }
+
+        /**
+         * Sends the {@link Message} using the provided {@link Handler}.
+         *
+         * @param handler The {@link Handler} to use for sending the message.
+         */
+        @Override
+        public void post(Handler handler) {
+            handler.sendMessage(mMessage);
+        }
+    }
+
+    /**
+     * A task that posts a {@link Runnable} to a {@link Handler}.
+     */
+    private static class RunnableTask extends Task {
+        private final Runnable mRunnable;
+
+        RunnableTask(Runnable runnable, long runTimeMs) {
+            super(runTimeMs);
+            mRunnable = runnable;
+        }
+
+        /**
+         * Posts the {@link Runnable} to the provided {@link Handler}.
+         *
+         * @param handler The {@link Handler} to use for posting the runnable.
+         */
+        @Override
+        public void post(Handler handler) {
+            handler.post(mRunnable);
+        }
+    }
+
+    /**
+     * The RealtimeScheduler constructor
+     *
+     * Note: The constructor is currently safe to call on another thread because it only sets final
+     * members and registers the event to be called on the handler.
+     */
+    public RealtimeScheduler(@NonNull Handler handler) {
+        mFdInt = TimerFdUtils.createTimerFileDescriptor();
+        mParcelFileDescriptor = ParcelFileDescriptor.adoptFd(mFdInt);
+        mHandler = handler;
+        mQueue = handler.getLooper().getQueue();
+        mTaskQueue = new PriorityQueue<>();
+        registerFdEventListener();
+
+        mGuard.open("close");
+    }
+
+    private boolean enqueueTask(@NonNull Task task, long delayMs) {
+        ensureRunningOnCorrectThread();
+        if (delayMs <= 0L) {
+            task.post(mHandler);
+            return true;
+        }
+        if (mTaskQueue.isEmpty() || task.compareTo(mTaskQueue.peek()) < 0) {
+            if (!TimerFdUtils.setExpirationTime(mFdInt, delayMs)) {
+                return false;
+            }
+        }
+        mTaskQueue.add(task);
+        return true;
+    }
+
+    /**
+     * Set a runnable to be executed after a specified delay.
+     *
+     * If delayMs is less than or equal to 0, the runnable will be executed immediately.
+     *
+     * @param runnable the runnable to be executed
+     * @param delayMs the delay time in milliseconds
+     * @return true if the task is scheduled successfully, false otherwise.
+     */
+    public boolean postDelayed(@NonNull Runnable runnable, long delayMs) {
+        return enqueueTask(new RunnableTask(runnable, SystemClock.elapsedRealtime() + delayMs),
+                delayMs);
+    }
+
+    /**
+     * Remove a scheduled runnable.
+     *
+     * @param runnable the runnable to be removed
+     */
+    public void removeDelayedRunnable(@NonNull Runnable runnable) {
+        ensureRunningOnCorrectThread();
+        mTaskQueue.removeIf(task -> task instanceof RunnableTask
+                && ((RunnableTask) task).mRunnable == runnable);
+    }
+
+    /**
+     * Set a message to be sent after a specified delay.
+     *
+     * If delayMs is less than or equal to 0, the message will be sent immediately.
+     *
+     * @param msg the message to be sent
+     * @param delayMs the delay time in milliseconds
+     * @return true if the message is scheduled successfully, false otherwise.
+     */
+    public boolean sendDelayedMessage(Message msg, long delayMs) {
+
+        return enqueueTask(new MessageTask(msg, SystemClock.elapsedRealtime() + delayMs), delayMs);
+    }
+
+    /**
+     * Remove a scheduled message.
+     *
+     * @param what the message to be removed
+     */
+    public void removeDelayedMessage(int what) {
+        ensureRunningOnCorrectThread();
+        mTaskQueue.removeIf(task -> task instanceof MessageTask
+                && ((MessageTask) task).mMessage.what == what);
+    }
+
+    /**
+     * Close the RealtimeScheduler. This implementation closes the underlying
+     * OS resources allocated to represent this stream.
+     */
+    public void close() {
+        ensureRunningOnCorrectThread();
+        unregisterAndDestroyFd();
+    }
+
+    private void registerFdEventListener() {
+        mQueue.addOnFileDescriptorEventListener(
+                mParcelFileDescriptor.getFileDescriptor(),
+                FD_EVENTS,
+                (fd, events) -> {
+                    if (!isRunning()) {
+                        return 0;
+                    }
+                    if ((events & EVENT_ERROR) != 0) {
+                        Log.wtf(TAG, "Got EVENT_ERROR from FileDescriptorEventListener.");
+                        return 0;
+                    }
+                    if ((events & EVENT_INPUT) != 0) {
+                        handleExpiration();
+                    }
+                    return FD_EVENTS;
+                });
+    }
+
+    private boolean isRunning() {
+        return mParcelFileDescriptor.getFileDescriptor().valid();
+    }
+
+    private void handleExpiration() {
+        // The data from the FileDescriptor must be read after the timer expires. Otherwise,
+        // expiration callbacks will continue to be sent, notifying of unread data. The content(the
+        // number of expirations) can be ignored, as the callback is the only item of interest.
+        // Refer to https://man7.org/linux/man-pages/man2/timerfd_create.2.html
+        // read(2)
+        //         If the timer has already expired one or more times since
+        //         its settings were last modified using timerfd_settime(),
+        //         or since the last successful read(2), then the buffer
+        //         given to read(2) returns an unsigned 8-byte integer
+        //         (uint64_t) containing the number of expirations that have
+        //         occurred.  (The returned value is in host byte order—that
+        //         is, the native byte order for integers on the host
+        //         machine.)
+        final byte[] readBuffer = new byte[8];
+        try {
+            Os.read(mParcelFileDescriptor.getFileDescriptor(), readBuffer, 0, readBuffer.length);
+        } catch (IOException | ErrnoException exception) {
+            Log.wtf(TAG, "Read FileDescriptor failed. ", exception);
+        }
+
+        long currentTimeMs = SystemClock.elapsedRealtime();
+        while (!mTaskQueue.isEmpty()) {
+            final Task task = mTaskQueue.peek();
+            currentTimeMs = SystemClock.elapsedRealtime();
+            if (currentTimeMs < task.getRunTimeMs()) {
+                break;
+            }
+            task.post(mHandler);
+            mTaskQueue.poll();
+        }
+
+        if (!mTaskQueue.isEmpty()) {
+            // Using currentTimeMs ensures that the calculated expiration time
+            // is always positive.
+            if (!TimerFdUtils.setExpirationTime(mFdInt,
+                    mTaskQueue.peek().getRunTimeMs() - currentTimeMs)) {
+                // If setting the expiration time fails, clear the task queue.
+                Log.wtf(TAG, "Failed to set expiration time");
+                mTaskQueue.clear();
+            }
+        }
+    }
+
+    private void unregisterAndDestroyFd() {
+        if (mGuard != null) {
+            mGuard.close();
+        }
+
+        mQueue.removeOnFileDescriptorEventListener(mParcelFileDescriptor.getFileDescriptor());
+        try {
+            mParcelFileDescriptor.close();
+        } catch (IOException exception) {
+            Log.e(TAG, "close ParcelFileDescriptor failed. ", exception);
+        }
+    }
+
+    private void ensureRunningOnCorrectThread() {
+        if (mHandler.getLooper() != Looper.myLooper()) {
+            throw new IllegalStateException(
+                    "Not running on Handler thread: " + Thread.currentThread().getName());
+        }
+    }
+
+    @SuppressWarnings("Finalize")
+    @Override
+    protected void finalize() throws Throwable {
+        if (mGuard != null) {
+            mGuard.warnIfOpen();
+        }
+        super.finalize();
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/ServiceConnectivityJni.java b/staticlibs/device/com/android/net/module/util/ServiceConnectivityJni.java
new file mode 100644
index 0000000..1d3561a
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/ServiceConnectivityJni.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import android.annotation.NonNull;
+import android.system.ErrnoException;
+
+/**
+ * Contains JNI functions for use in service-connectivity
+ */
+public class ServiceConnectivityJni {
+    static {
+        final String libName = JniUtil.getJniLibraryName(ServiceConnectivityJni.class.getPackage());
+        if (libName.equals("android_net_connectivity_com_android_net_module_util_jni")) {
+            // This library is part of service-connectivity.jar when in the system server,
+            // so libservice-connectivity.so is the library to load.
+            System.loadLibrary("service-connectivity");
+        } else {
+            System.loadLibrary(libName);
+        }
+    }
+
+    /**
+     * Create a timerfd.
+     *
+     * @throws ErrnoException if the timerfd creation is failed.
+     */
+    public static native int createTimerFd() throws ErrnoException;
+
+    /**
+     * Set given time to the timerfd.
+     *
+     * @param timeMs target time
+     * @throws ErrnoException if setting expiration time is failed.
+     */
+    public static native void setTimerFdTime(int fd, long timeMs) throws ErrnoException;
+
+    /** Create tun/tap interface */
+    public static native int createTunTap(boolean isTun, boolean hasCarrier,
+            boolean setIffMulticast, @NonNull String iface);
+
+    /** Enable carrier on tun/tap interface */
+    public static native void setTunTapCarrierEnabled(@NonNull String iface, int tunFd,
+            boolean enabled);
+
+    /** Bring up tun/tap interface */
+    public static native void bringUpInterface(String iface);
+}
diff --git a/staticlibs/device/com/android/net/module/util/SkDestroyListener.java b/staticlibs/device/com/android/net/module/util/SkDestroyListener.java
new file mode 100644
index 0000000..c7c2829
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/SkDestroyListener.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import static android.system.OsConstants.NETLINK_INET_DIAG;
+
+import android.os.Handler;
+
+import com.android.net.module.util.ip.NetlinkMonitor;
+import com.android.net.module.util.netlink.InetDiagMessage;
+import com.android.net.module.util.netlink.NetlinkMessage;
+
+import java.io.PrintWriter;
+import java.util.function.Consumer;
+
+/**
+ * Monitor socket destroy and delete entry from cookie tag bpf map.
+ */
+public class SkDestroyListener extends NetlinkMonitor {
+    private static final int SKNLGRP_INET_TCP_DESTROY = 1;
+    private static final int SKNLGRP_INET_UDP_DESTROY = 2;
+    private static final int SKNLGRP_INET6_TCP_DESTROY = 3;
+    private static final int SKNLGRP_INET6_UDP_DESTROY = 4;
+
+    // TODO: if too many sockets are closed too quickly, this can overflow the socket buffer, and
+    // some entries in mCookieTagMap will not be freed. In order to fix this it would be needed to
+    // periodically dump all sockets and remove the tag entries for sockets that have been closed.
+    // For now, set a large-enough buffer that hundreds of sockets can be closed without getting
+    // ENOBUFS and leaking mCookieTagMap entries.
+    private static final int SOCK_RCV_BUF_SIZE = 512 * 1024;
+
+    private final Consumer<InetDiagMessage> mSkDestroyCallback;
+
+    /**
+     * Return SkDestroyListener that monitor both TCP and UDP socket destroy
+     *
+     * @param consumer The consumer that processes InetDiagMessage
+     * @param handler The Handler on which to poll for messages
+     * @param log A SharedLog to log to.
+     * @return SkDestroyListener
+     */
+    public static SkDestroyListener makeSkDestroyListener(final Consumer<InetDiagMessage> consumer,
+            final Handler handler, final SharedLog log) {
+        return makeSkDestroyListener(consumer, true /* monitorTcpSocket */,
+                true /* monitorUdpSocket */, handler, log);
+    }
+
+    /**
+     * Return SkDestroyListener that monitor socket destroy
+     *
+     * @param consumer The consumer that processes InetDiagMessage
+     * @param monitorTcpSocket {@code true} to monitor TCP socket destroy
+     * @param monitorUdpSocket {@code true} to monitor UDP socket destroy
+     * @param handler The Handler on which to poll for messages
+     * @param log A SharedLog to log to.
+     * @return SkDestroyListener
+     */
+    public static SkDestroyListener makeSkDestroyListener(final Consumer<InetDiagMessage> consumer,
+            final boolean monitorTcpSocket, final boolean monitorUdpSocket,
+            final Handler handler, final SharedLog log) {
+        if (!monitorTcpSocket && !monitorUdpSocket) {
+            throw new IllegalArgumentException(
+                    "Both monitorTcpSocket and monitorUdpSocket can not be false");
+        }
+        int bindGroups = 0;
+        if (monitorTcpSocket) {
+            bindGroups |= 1 << (SKNLGRP_INET_TCP_DESTROY - 1)
+                    | 1 << (SKNLGRP_INET6_TCP_DESTROY - 1);
+        }
+        if (monitorUdpSocket) {
+            bindGroups |= 1 << (SKNLGRP_INET_UDP_DESTROY - 1)
+                    | 1 << (SKNLGRP_INET6_UDP_DESTROY - 1);
+        }
+        return new SkDestroyListener(consumer, bindGroups, handler, log);
+    }
+
+    private SkDestroyListener(final Consumer<InetDiagMessage> consumer, final int bindGroups,
+            final Handler handler, final SharedLog log) {
+        super(handler, log, "SkDestroyListener", NETLINK_INET_DIAG,
+                bindGroups, SOCK_RCV_BUF_SIZE);
+        mSkDestroyCallback = consumer;
+    }
+
+    @Override
+    public void processNetlinkMessage(final NetlinkMessage nlMsg, final long whenMs) {
+        if (!(nlMsg instanceof InetDiagMessage)) {
+            mLog.e("Received non InetDiagMessage");
+            return;
+        }
+        mSkDestroyCallback.accept((InetDiagMessage) nlMsg);
+    }
+
+    /**
+     * Dump the contents of SkDestroyListener log.
+     */
+    public void dump(PrintWriter pw) {
+        mLog.reverseDump(pw);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/TimerFdUtils.java b/staticlibs/device/com/android/net/module/util/TimerFdUtils.java
index 310dbc9..cce7efd 100644
--- a/staticlibs/device/com/android/net/module/util/TimerFdUtils.java
+++ b/staticlibs/device/com/android/net/module/util/TimerFdUtils.java
@@ -16,49 +16,22 @@
 
 package com.android.net.module.util;
 
-import android.os.Process;
+import android.system.ErrnoException;
 import android.util.Log;
 
-import java.io.IOException;
-
 /**
  * Contains mostly timerfd functionality.
  */
 public class TimerFdUtils {
-    static {
-        if (Process.myUid() == Process.SYSTEM_UID) {
-            // This library is part of service-connectivity.jar when in the system server,
-            // so libservice-connectivity.so is the library to load.
-            System.loadLibrary("service-connectivity");
-        } else {
-            System.loadLibrary(JniUtil.getJniLibraryName(TimerFdUtils.class.getPackage()));
-        }
-    }
-
     private static final String TAG = TimerFdUtils.class.getSimpleName();
 
     /**
-     * Create a timerfd.
-     *
-     * @throws IOException if the timerfd creation is failed.
-     */
-    private static native int createTimerFd() throws IOException;
-
-    /**
-     * Set given time to the timerfd.
-     *
-     * @param timeMs target time
-     * @throws IOException if setting expiration time is failed.
-     */
-    private static native void setTime(int fd, long timeMs) throws IOException;
-
-    /**
      * Create a timerfd
      */
     static int createTimerFileDescriptor() {
         try {
-            return createTimerFd();
-        } catch (IOException e) {
+            return ServiceConnectivityJni.createTimerFd();
+        } catch (ErrnoException e) {
             Log.e(TAG, "createTimerFd failed", e);
             return -1;
         }
@@ -67,10 +40,10 @@
     /**
      * Set expiration time to timerfd
      */
-    static boolean setExpirationTime(int id, long expirationTimeMs) {
+    static boolean setExpirationTime(int fd, long expirationTimeMs) {
         try {
-            setTime(id, expirationTimeMs);
-        } catch (IOException e) {
+            ServiceConnectivityJni.setTimerFdTime(fd, expirationTimeMs);
+        } catch (ErrnoException e) {
             Log.e(TAG, "setExpirationTime failed", e);
             return false;
         }
diff --git a/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java b/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
index fecaa09..c9a89ec 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
@@ -309,16 +309,18 @@
     }
 
     private static void sendNetlinkDestroyRequest(FileDescriptor fd, int proto,
-            InetDiagMessage diagMsg) throws InterruptedIOException, ErrnoException {
+            StructInetDiagSockId id, short family, int state)
+            throws InterruptedIOException, ErrnoException {
+        // TODO: Investigate if it's fine to always set 0 to state and remove state from the arg
         final byte[] destroyMsg = InetDiagMessage.inetDiagReqV2(
                 proto,
-                diagMsg.inetDiagMsg.id,
-                diagMsg.inetDiagMsg.idiag_family,
+                id,
+                family,
                 SOCK_DESTROY,
                 (short) (StructNlMsgHdr.NLM_F_REQUEST | StructNlMsgHdr.NLM_F_ACK),
                 0 /* pad */,
                 0 /* idiagExt */,
-                1 << diagMsg.inetDiagMsg.idiag_state
+                state
         );
         NetlinkUtils.sendMessage(fd, destroyMsg, 0, destroyMsg.length, IO_TIMEOUT_MS);
         NetlinkUtils.receiveNetlinkAck(fd);
@@ -343,7 +345,8 @@
         Consumer<InetDiagMessage> handleNlDumpMsg = (diagMsg) -> {
             if (filter.test(diagMsg)) {
                 try {
-                    sendNetlinkDestroyRequest(destroyFd, proto, diagMsg);
+                    sendNetlinkDestroyRequest(destroyFd, proto, diagMsg.inetDiagMsg.id,
+                            diagMsg.inetDiagMsg.idiag_family, 1 << diagMsg.inetDiagMsg.idiag_state);
                     destroyedSockets.getAndIncrement();
                 } catch (InterruptedIOException | ErrnoException e) {
                     if (!(e instanceof ErrnoException
@@ -484,6 +487,30 @@
         Log.d(TAG, "Destroyed live tcp sockets for uids=" + ownerUids + " in " + durationMs + "ms");
     }
 
+    /**
+     * Close the udp socket which can be uniquely identified with the cookie and other information.
+     */
+    public static void destroyUdpSocket(final InetSocketAddress src, final InetSocketAddress dst,
+            final int ifIndex, final long cookie)
+            throws ErrnoException, SocketException, InterruptedIOException {
+        FileDescriptor fd = null;
+
+        try {
+            fd = NetlinkUtils.createNetLinkInetDiagSocket();
+            connectToKernel(fd);
+            final int family = (src.getAddress() instanceof Inet6Address) ? AF_INET6 : AF_INET;
+            final StructInetDiagSockId id = new StructInetDiagSockId(
+                    src,
+                    dst,
+                    ifIndex,
+                    cookie
+            );
+            sendNetlinkDestroyRequest(fd, IPPROTO_UDP, id, (short) family, 0 /* state */);
+        } finally {
+            closeSocketQuietly(fd);
+        }
+    }
+
     @Override
     public String toString() {
         return "InetDiagMessage{ "
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
index 781a04e..dfb2053 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
@@ -150,6 +150,8 @@
                 return (NetlinkMessage) RtNetlinkNeighborMessage.parse(nlmsghdr, byteBuffer);
             case NetlinkConstants.RTM_NEWNDUSEROPT:
                 return (NetlinkMessage) NduseroptMessage.parse(nlmsghdr, byteBuffer);
+            case NetlinkConstants.RTM_NEWPREFIX:
+                return (NetlinkMessage) RtNetlinkPrefixMessage.parse(nlmsghdr, byteBuffer);
             default: return null;
         }
     }
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
index 541a375..2420e7a 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
@@ -55,6 +55,7 @@
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
 import java.util.function.Consumer;
@@ -82,7 +83,7 @@
     public static final int INET_DIAG_INFO = 2;
     public static final int INET_DIAG_MARK = 15;
 
-    public static final long IO_TIMEOUT_MS = 300L;
+    public static final long IO_TIMEOUT_MS = 3000L;
 
     public static final int DEFAULT_RECV_BUFSIZE = 8 * 1024;
     public static final int SOCKET_RECV_BUFSIZE = 64 * 1024;
@@ -469,4 +470,60 @@
             // Nothing we can do here
         }
     }
+
+    /**
+     * Sends a netlink request to set flags for given interface
+     *
+     * @param interfaceName The name of the network interface to query.
+     * @param flags power-of-two integer flags to set or unset. A flag to set should be passed as
+     *        is as a power-of-two value, and a flag to remove should be passed inversed as -1 with
+     *        a single bit down. For example: IFF_UP, ~IFF_BROADCAST...
+     * @return true if the request finished successfully, otherwise false.
+     */
+    public static boolean setInterfaceFlags(@NonNull String interfaceName, int... flags) {
+        final RtNetlinkLinkMessage ntMsg =
+                RtNetlinkLinkMessage.createSetFlagsMessage(interfaceName, /*seqNo*/ 0, flags);
+        if (ntMsg == null) {
+            Log.e(TAG, "Failed to create message to set interface flags for interface "
+                    + interfaceName + ", input flags are: " + Arrays.toString(flags));
+            return false;
+        }
+        final byte[] msg = ntMsg.pack(ByteOrder.nativeOrder());
+        try {
+            NetlinkUtils.sendOneShotKernelMessage(NETLINK_ROUTE, msg);
+            return true;
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Failed to set flags for: " + interfaceName, e);
+            return false;
+        }
+    }
+
+    /**
+     * Sends a netlink request to set MTU for given interface
+     *
+     * @param interfaceName The name of the network interface to query.
+     * @param mtu MTU value to set for the interface.
+     * @return true if the request finished successfully, otherwise false.
+     */
+    public static boolean setInterfaceMtu(@NonNull String interfaceName, int mtu) {
+        if (mtu < 68) {
+            Log.e(TAG, "Invalid mtu: " + mtu + ", mtu should be greater than 68 referring RFC791");
+            return false;
+        }
+        final RtNetlinkLinkMessage ntMsg =
+                RtNetlinkLinkMessage.createSetMtuMessage(interfaceName, /*seqNo*/ 0, mtu);
+        if (ntMsg == null) {
+            Log.e(TAG, "Failed to create message to set MTU to " + mtu
+                    + "for interface " + interfaceName);
+            return false;
+        }
+        final byte[] msg = ntMsg.pack(ByteOrder.nativeOrder());
+        try {
+            NetlinkUtils.sendOneShotKernelMessage(NETLINK_ROUTE, msg);
+            return true;
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Failed to set MTU to " + mtu + " for: " + interfaceName, e);
+            return false;
+        }
+    }
 }
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
index 037d95f..c19a124 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
@@ -312,6 +312,89 @@
                 DEFAULT_MTU, null, null);
     }
 
+    /**
+     * Creates an {@link RtNetlinkLinkMessage} instance that can be used to set the flags of a
+     * network interface.
+     *
+     * @param interfaceName The name of the network interface to query.
+     * @param sequenceNumber The sequence number for the Netlink message.
+     * @param flags power-of-two integer flags to set or unset. A flag to set should be passed as
+     *        is as a power-of-two value, and a flag to remove should be passed inversed as -1 with
+     *        a single bit down. For example: IFF_UP, ~IFF_BROADCAST...
+     * @return An `RtNetlinkLinkMessage` instance representing the request to query the interface.
+     */
+    @Nullable
+    public static RtNetlinkLinkMessage createSetFlagsMessage(@NonNull String interfaceName,
+            int sequenceNumber, int... flags) {
+        return createSetFlagsMessage(
+                interfaceName, sequenceNumber, new OsAccess(), flags);
+    }
+
+    @VisibleForTesting
+    @Nullable
+    protected static RtNetlinkLinkMessage createSetFlagsMessage(
+            @NonNull String interfaceName, int sequenceNumber, @NonNull OsAccess osAccess,
+            int... flags) {
+        final int interfaceIndex = osAccess.if_nametoindex(interfaceName);
+        if (interfaceIndex == OsAccess.INVALID_INTERFACE_INDEX) {
+            return null;
+        }
+
+        int flagsBits = 0;
+        int changeBits = 0;
+        for (int f : flags) {
+            if (Integer.bitCount(f) == 1) {
+                flagsBits |= f;
+                changeBits |= f;
+            } else if (Integer.bitCount(~f) == 1) {
+                flagsBits &= f;
+                changeBits |= ~f;
+            } else {
+                return null;
+            }
+        }
+        // RTM_NEWLINK is used here for create, modify, or notify changes about a internet
+        // interface, including change in administrative state. While RTM_SETLINK is used to
+        // modify an existing link rather than creating a new one.
+        return RtNetlinkLinkMessage.build(
+                new StructNlMsgHdr(
+                        /*payloadLen*/ 0, RTM_NEWLINK, NLM_F_REQUEST_ACK, sequenceNumber),
+                new StructIfinfoMsg((short) AF_UNSPEC, /*type*/ 0, interfaceIndex,
+                        flagsBits, changeBits),
+                DEFAULT_MTU, /*hardwareAddress*/ null, /*interfaceName*/ null);
+    }
+
+    /**
+     * Creates an {@link RtNetlinkLinkMessage} instance that can be used to set the MTU of a
+     * network interface.
+     *
+     * @param interfaceName The name of the network interface to query.
+     * @param sequenceNumber The sequence number for the Netlink message.
+     * @param mtu MTU value to set for the interface.
+     * @return An `RtNetlinkLinkMessage` instance representing the request to query the interface.
+     */
+    @Nullable
+    public static RtNetlinkLinkMessage createSetMtuMessage(@NonNull String interfaceName,
+            int sequenceNumber, int mtu) {
+        return createSetMtuMessage(
+            interfaceName, sequenceNumber, mtu, new OsAccess());
+    }
+
+    @VisibleForTesting
+    @Nullable
+    protected static RtNetlinkLinkMessage createSetMtuMessage(@NonNull String interfaceName,
+            int sequenceNumber, int mtu, @NonNull OsAccess osAccess) {
+        final int interfaceIndex = osAccess.if_nametoindex(interfaceName);
+        if (interfaceIndex == OsAccess.INVALID_INTERFACE_INDEX) {
+            return null;
+        }
+        return RtNetlinkLinkMessage.build(
+            new StructNlMsgHdr(/*payloadLen*/ 0, RTM_NEWLINK, NLM_F_REQUEST_ACK , sequenceNumber),
+            new StructIfinfoMsg((short) AF_UNSPEC, /*type*/ 0, interfaceIndex,
+                /*flags*/ 0, /*change*/ 0),
+            mtu, /*hardwareAddress*/ null, /*interfaceName*/ null);
+    }
+
     @Override
     public String toString() {
         return "RtNetlinkLinkMessage{ "
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkPrefixMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkPrefixMessage.java
new file mode 100644
index 0000000..30c63fb
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkPrefixMessage.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink;
+
+import android.net.IpPrefix;
+import android.system.OsConstants;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import java.net.Inet6Address;
+import java.nio.ByteBuffer;
+
+/**
+ * A NetlinkMessage subclass for rtnetlink address messages.
+ *
+ * RtNetlinkPrefixMessage.parse() must be called with a ByteBuffer that contains exactly one
+ * netlink message.
+ *
+ * see also:
+ *
+ *     include/uapi/linux/rtnetlink.h
+ *
+ * @hide
+ */
+public class RtNetlinkPrefixMessage extends NetlinkMessage {
+    public static final short PREFIX_ADDRESS       = 1;
+    public static final short PREFIX_CACHEINFO     = 2;
+
+    @NonNull
+    private StructPrefixMsg mPrefixmsg;
+    @NonNull
+    private IpPrefix mPrefix;
+    private long mPreferredLifetime;
+    private long mValidLifetime;
+
+    @VisibleForTesting
+    public RtNetlinkPrefixMessage(@NonNull final StructNlMsgHdr header,
+            @NonNull final StructPrefixMsg prefixmsg,
+            @NonNull final IpPrefix prefix,
+            long preferred, long valid) {
+        super(header);
+        mPrefixmsg = prefixmsg;
+        mPrefix = prefix;
+        mPreferredLifetime = preferred;
+        mValidLifetime = valid;
+    }
+
+    private RtNetlinkPrefixMessage(@NonNull StructNlMsgHdr header) {
+        this(header, null, null, 0 /* preferredLifetime */, 0 /* validLifetime */);
+    }
+
+    @NonNull
+    public StructPrefixMsg getPrefixMsg() {
+        return mPrefixmsg;
+    }
+
+    @NonNull
+    public IpPrefix getPrefix() {
+        return mPrefix;
+    }
+
+    public long getPreferredLifetime() {
+        return mPreferredLifetime;
+    }
+
+    public long getValidLifetime() {
+        return mValidLifetime;
+    }
+
+    /**
+     * Parse rtnetlink prefix message from {@link ByteBuffer}. This method must be called with a
+     * ByteBuffer that contains exactly one netlink message.
+     *
+     * RTM_NEWPREFIX Message Format:
+     *  +----------+- - -+-------------+- - -+---------------------+-----------------------+
+     *  | nlmsghdr | Pad |  prefixmsg  | Pad | PREFIX_ADDRESS attr | PREFIX_CACHEINFO attr |
+     *  +----------+- - -+-------------+- - -+---------------------+-----------------------+
+     *
+     * @param header netlink message header.
+     * @param byteBuffer the ByteBuffer instance that wraps the raw netlink message bytes.
+     */
+    @Nullable
+    public static RtNetlinkPrefixMessage parse(@NonNull final StructNlMsgHdr header,
+            @NonNull final ByteBuffer byteBuffer) {
+        try {
+            final RtNetlinkPrefixMessage msg = new RtNetlinkPrefixMessage(header);
+            msg.mPrefixmsg = StructPrefixMsg.parse(byteBuffer);
+
+            // PREFIX_ADDRESS
+            final int baseOffset = byteBuffer.position();
+            StructNlAttr nlAttr = StructNlAttr.findNextAttrOfType(PREFIX_ADDRESS, byteBuffer);
+            if (nlAttr == null) return null;
+            final Inet6Address addr = (Inet6Address) nlAttr.getValueAsInetAddress();
+            if (addr == null) return null;
+            msg.mPrefix = new IpPrefix(addr, msg.mPrefixmsg.prefix_len);
+
+            // PREFIX_CACHEINFO
+            byteBuffer.position(baseOffset);
+            nlAttr = StructNlAttr.findNextAttrOfType(PREFIX_CACHEINFO, byteBuffer);
+            if (nlAttr == null) return null;
+            final ByteBuffer buffer = nlAttr.getValueAsByteBuffer();
+            if (buffer == null) return null;
+            final StructPrefixCacheInfo cacheinfo = StructPrefixCacheInfo.parse(buffer);
+            msg.mPreferredLifetime = cacheinfo.preferred_time;
+            msg.mValidLifetime = cacheinfo.valid_time;
+
+            return msg;
+        } catch (IllegalArgumentException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Write a rtnetlink prefix message to {@link ByteBuffer}.
+     */
+    @VisibleForTesting
+    protected void pack(ByteBuffer byteBuffer) {
+        getHeader().pack(byteBuffer);
+        mPrefixmsg.pack(byteBuffer);
+
+        // PREFIX_ADDRESS attribute
+        final StructNlAttr prefixAddress =
+                new StructNlAttr(PREFIX_ADDRESS, mPrefix.getRawAddress());
+        prefixAddress.pack(byteBuffer);
+
+        // PREFIX_CACHEINFO attribute
+        final StructPrefixCacheInfo cacheinfo =
+                new StructPrefixCacheInfo(mPreferredLifetime, mValidLifetime);
+        final StructNlAttr prefixCacheinfo =
+                new StructNlAttr(PREFIX_CACHEINFO, cacheinfo.writeToBytes());
+        prefixCacheinfo.pack(byteBuffer);
+    }
+
+    @Override
+    public String toString() {
+        return "RtNetlinkPrefixMessage{ "
+                + "nlmsghdr{" + mHeader.toString(OsConstants.NETLINK_ROUTE) + "}, "
+                + "prefixmsg{" + mPrefixmsg.toString() + "}, "
+                + "IP Prefix{" + mPrefix + "}, "
+                + "preferred lifetime{" + mPreferredLifetime + "}, "
+                + "valid lifetime{" + mValidLifetime + "} "
+                + "}";
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/CollectionUtils.java b/staticlibs/framework/com/android/net/module/util/CollectionUtils.java
index f3d8c4a..760d849 100644
--- a/staticlibs/framework/com/android/net/module/util/CollectionUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/CollectionUtils.java
@@ -22,6 +22,7 @@
 import android.util.Pair;
 import android.util.SparseArray;
 
+import java.lang.reflect.Array;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
@@ -413,4 +414,68 @@
         }
         return -1;
     }
+
+    /**
+     * Concatenates multiple arrays of the same type into a single new array.
+     */
+    public static byte[] concatArrays(@NonNull byte[]... arr) {
+        int size = 0;
+        for (byte[] a : arr) {
+            size += a.length;
+        }
+        final byte[] result = new byte[size];
+        int offset = 0;
+        for (byte[] a : arr) {
+            System.arraycopy(a, 0, result, offset, a.length);
+            offset += a.length;
+        }
+        return result;
+    }
+
+    /**
+     * Concatenates multiple arrays of the same type into a single new array.
+     */
+    public static <T> T[] concatArrays(@NonNull Class<T> clazz, @NonNull T[]... arr) {
+        int size = 0;
+        for (T[] a : arr) {
+            size += a.length;
+        }
+        final T[] result = (T[]) Array.newInstance(clazz, size);
+        int offset = 0;
+        for (T[] a : arr) {
+            System.arraycopy(a, 0, result, offset, a.length);
+            offset += a.length;
+        }
+        return result;
+    }
+
+    /**
+     * Prepends the elements of a variable number of prefixes to an existing array (suffix).
+     */
+    public static byte[] prependArray(@NonNull byte[] suffix, @NonNull byte... prefixes) {
+        return concatArrays(prefixes, suffix);
+    }
+
+    /**
+     * Prepends the elements of a variable number of prefixes to an existing array (suffix).
+     */
+    public static <T> T[] prependArray(@NonNull Class<T> clazz, @NonNull T[] suffix,
+            @NonNull T... prefixes) {
+        return concatArrays(clazz, prefixes, suffix);
+    }
+
+    /**
+     * Appends the elements of a variable number of suffixes to an existing array (prefix).
+     */
+    public static byte[] appendArray(@NonNull byte[] prefix, @NonNull byte... suffixes) {
+        return concatArrays(prefix, suffixes);
+    }
+
+    /**
+     * Appends the elements of a variable number of suffixes to an existing array (prefix).
+     */
+    public static <T> T[] appendArray(@NonNull Class<T> clazz, @NonNull T[] prefix,
+            @NonNull T... suffixes) {
+        return concatArrays(clazz, prefix, suffixes);
+    }
 }
diff --git a/staticlibs/framework/com/android/net/module/util/HexDump.java b/staticlibs/framework/com/android/net/module/util/HexDump.java
index a22c258..409f611 100644
--- a/staticlibs/framework/com/android/net/module/util/HexDump.java
+++ b/staticlibs/framework/com/android/net/module/util/HexDump.java
@@ -202,7 +202,7 @@
         if (c >= 'A' && c <= 'F') return (c - 'A' + 10);
         if (c >= 'a' && c <= 'f') return (c - 'a' + 10);
 
-        throw new RuntimeException("Invalid hex char '" + c + "'");
+        throw new IllegalArgumentException("Invalid hex char '" + c + "'");
     }
 
     /**
diff --git a/staticlibs/framework/com/android/net/module/util/LruCacheWithExpiry.java b/staticlibs/framework/com/android/net/module/util/LruCacheWithExpiry.java
index 80088b9..96d995a 100644
--- a/staticlibs/framework/com/android/net/module/util/LruCacheWithExpiry.java
+++ b/staticlibs/framework/com/android/net/module/util/LruCacheWithExpiry.java
@@ -22,13 +22,13 @@
 
 import com.android.internal.annotations.GuardedBy;
 
-import java.time.Clock;
 import java.util.Objects;
+import java.util.function.LongSupplier;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
 
 /**
- * An LRU cache that stores key-value pairs with an expiry time.
+ * A thread-safe LRU cache that stores key-value pairs with an expiry time.
  *
  * <p>This cache uses an {@link LruCache} to store entries and evicts the least
  * recently used entries when the cache reaches its maximum capacity. It also
@@ -41,7 +41,7 @@
  * @hide
  */
 public class LruCacheWithExpiry<K, V> {
-    private final Clock mClock;
+    private final LongSupplier mTimeSupplier;
     private final long mExpiryDurationMs;
     @GuardedBy("mMap")
     private final LruCache<K, CacheValue<V>> mMap;
@@ -50,16 +50,17 @@
     /**
      * Constructs a new {@link LruCacheWithExpiry} with the specified parameters.
      *
-     * @param clock            The {@link Clock} to use for determining timestamps.
+     * @param timeSupplier     The {@link java.util.function.LongSupplier} to use for
+     *                         determining timestamps.
      * @param expiryDurationMs The expiry duration for cached entries in milliseconds.
      * @param maxSize          The maximum number of entries to hold in the cache.
      * @param shouldCacheValue A {@link Predicate} that determines whether a given value should be
      *                         cached. This can be used to filter out certain values from being
      *                         stored in the cache.
      */
-    public LruCacheWithExpiry(@NonNull Clock clock, long expiryDurationMs, int maxSize,
-            Predicate<V> shouldCacheValue) {
-        mClock = clock;
+    public LruCacheWithExpiry(@NonNull LongSupplier timeSupplier, long expiryDurationMs,
+            int maxSize, Predicate<V> shouldCacheValue) {
+        mTimeSupplier = timeSupplier;
         mExpiryDurationMs = expiryDurationMs;
         mMap = new LruCache<>(maxSize);
         mShouldCacheValue = shouldCacheValue;
@@ -119,7 +120,26 @@
     public void put(@NonNull K key, @NonNull V value) {
         Objects.requireNonNull(value);
         synchronized (mMap) {
-            mMap.put(key, new CacheValue<>(mClock.millis(), value));
+            mMap.put(key, new CacheValue<>(mTimeSupplier.getAsLong(), value));
+        }
+    }
+
+    /**
+     * Stores a value in the cache if absent, associated with the given key.
+     *
+     * @param key   The key to associate with the value.
+     * @param value The value to store in the cache.
+     * @return The existing value associated with the key, if present; otherwise, null.
+     */
+    @Nullable
+    public V putIfAbsent(@NonNull K key, @NonNull V value) {
+        Objects.requireNonNull(value);
+        synchronized (mMap) {
+            final V existingValue = get(key);
+            if (existingValue == null) {
+                put(key, value);
+            }
+            return existingValue;
         }
     }
 
@@ -133,7 +153,7 @@
     }
 
     private boolean isExpired(long timestamp) {
-        return mClock.millis() > timestamp + mExpiryDurationMs;
+        return mTimeSupplier.getAsLong() > timestamp + mExpiryDurationMs;
     }
 
     private static class CacheValue<V> {
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
index 5d588cc..8d7ae5c 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
@@ -23,6 +23,7 @@
 import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
+import java.util.List;
 
 /**
  * Network constants used by the network stack.
@@ -120,6 +121,8 @@
             (byte) 0, (byte) 0, (byte) 0, (byte) 0,
             (byte) 0, (byte) 0, (byte) 0, (byte) 0,
             (byte) 0, (byte) 0, (byte) 0, (byte) 0 });
+    public static final Inet4Address IPV4_ADDR_ALL_HOST_MULTICAST =
+            (Inet4Address) InetAddresses.parseNumericAddress("224.0.0.1");
 
     /**
      * CLAT constants
@@ -151,7 +154,8 @@
             (Inet6Address) InetAddresses.parseNumericAddress("ff02::2");
     public static final Inet6Address IPV6_ADDR_ALL_HOSTS_MULTICAST =
             (Inet6Address) InetAddresses.parseNumericAddress("ff02::3");
-
+    public static final Inet6Address IPV6_ADDR_NODE_LOCAL_ALL_NODES_MULTICAST =
+             (Inet6Address) InetAddresses.parseNumericAddress("ff01::1");
     public static final int IPPROTO_FRAGMENT = 44;
 
     /**
@@ -338,6 +342,26 @@
      */
     public static final String TEST_URL_EXPIRATION_TIME = "test_url_expiration_time";
 
+    /**
+     * List of IpPrefix that are local network prefixes.
+     */
+    public static final List<IpPrefix> IPV4_LOCAL_PREFIXES = List.of(
+            new IpPrefix("169.254.0.0/16"), // Link Local
+            new IpPrefix("100.64.0.0/10"),  // CGNAT
+            new IpPrefix("10.0.0.0/8"),     // RFC1918
+            new IpPrefix("172.16.0.0/12"),  // RFC1918
+            new IpPrefix("192.168.0.0/16")  // RFC1918
+    );
+
+    /**
+     * List of IpPrefix that are multicast and broadcast prefixes.
+     */
+    public static final List<IpPrefix> MULTICAST_AND_BROADCAST_PREFIXES = List.of(
+            new IpPrefix("224.0.0.0/4"),               // Multicast
+            new IpPrefix("ff00::/8"),                  // Multicast
+            new IpPrefix("255.255.255.255/32")         // Broadcast
+    );
+
     // TODO: Move to Inet4AddressUtils
     // See aosp/1455936: NetworkStackConstants can't depend on it as it causes jarjar-related issues
     // for users of both the net-utils-device-common and net-utils-framework-common libraries.
diff --git a/staticlibs/framework/com/android/net/module/util/TerribleErrorLog.java b/staticlibs/framework/com/android/net/module/util/TerribleErrorLog.java
new file mode 100644
index 0000000..b4f7642
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/TerribleErrorLog.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import android.annotation.NonNull;
+import android.util.Log;
+
+import java.util.function.BiConsumer;
+
+/**
+ * Utility class for logging terrible errors and reporting them for tracking.
+ *
+ * @hide
+ */
+public class TerribleErrorLog {
+
+    private static final String TAG = TerribleErrorLog.class.getSimpleName();
+
+    /**
+     * Logs a terrible error and reports metrics through a provided statsLog.
+     */
+    public static void logTerribleError(@NonNull BiConsumer<Integer, Integer> statsLog,
+            @NonNull String message, int protoType, int errorType) {
+        statsLog.accept(protoType, errorType);
+        Log.wtf(TAG, message);
+    }
+}
diff --git a/staticlibs/native/timerfdutils/Android.bp b/staticlibs/native/serviceconnectivityjni/Android.bp
similarity index 86%
rename from staticlibs/native/timerfdutils/Android.bp
rename to staticlibs/native/serviceconnectivityjni/Android.bp
index 939a2d2..18246dd 100644
--- a/staticlibs/native/timerfdutils/Android.bp
+++ b/staticlibs/native/serviceconnectivityjni/Android.bp
@@ -18,17 +18,20 @@
 }
 
 cc_library_static {
-    name: "libnet_utils_device_common_timerfdjni",
+    name: "libserviceconnectivityjni",
     srcs: [
-        "com_android_net_module_util_TimerFdUtils.cpp",
+        "com_android_net_module_util_ServiceConnectivityJni.cpp",
     ],
     header_libs: [
+        "bpf_headers",
         "jni_headers",
+        "libbase_headers",
     ],
     shared_libs: [
         "liblog",
         "libnativehelper_compat_libc++",
     ],
+    stl: "libc++_static",
     cflags: [
         "-Wall",
         "-Werror",
diff --git a/staticlibs/native/serviceconnectivityjni/com_android_net_module_util_ServiceConnectivityJni.cpp b/staticlibs/native/serviceconnectivityjni/com_android_net_module_util_ServiceConnectivityJni.cpp
new file mode 100644
index 0000000..8767589
--- /dev/null
+++ b/staticlibs/native/serviceconnectivityjni/com_android_net_module_util_ServiceConnectivityJni.cpp
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <jni.h>
+#include <linux/if.h>
+#include <linux/if_tun.h>
+#include <linux/ipv6_route.h>
+#include <linux/route.h>
+#include <netinet/in.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <string>
+#include <sys/epoll.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/timerfd.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <android-base/unique_fd.h>
+#include <bpf/KernelUtils.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/scoped_utf_chars.h>
+
+#define MSEC_PER_SEC 1000
+#define NSEC_PER_MSEC 1000000
+
+#ifndef IFF_NO_CARRIER
+#define IFF_NO_CARRIER 0x0040
+#endif
+
+namespace android {
+
+static jint createTimerFd(JNIEnv *env, jclass clazz) {
+  int tfd;
+  // For safety, the file descriptor should have O_NONBLOCK(TFD_NONBLOCK) set
+  // using fcntl during creation. This ensures that, in the worst-case scenario,
+  // an EAGAIN error is returned when reading.
+  tfd = timerfd_create(CLOCK_BOOTTIME, TFD_NONBLOCK);
+  if (tfd == -1) {
+    jniThrowErrnoException(env, "createTimerFd", tfd);
+  }
+  return tfd;
+}
+
+static void setTimerFdTime(JNIEnv *env, jclass clazz, jint tfd,
+                           jlong milliseconds) {
+  struct itimerspec new_value;
+  new_value.it_value.tv_sec = milliseconds / MSEC_PER_SEC;
+  new_value.it_value.tv_nsec = (milliseconds % MSEC_PER_SEC) * NSEC_PER_MSEC;
+  // Set the interval time to 0 because it's designed for repeated timer
+  // expirations after the initial expiration, which doesn't fit the current
+  // usage.
+  new_value.it_interval.tv_sec = 0;
+  new_value.it_interval.tv_nsec = 0;
+
+  int ret = timerfd_settime(tfd, 0, &new_value, NULL);
+  if (ret == -1) {
+    jniThrowErrnoException(env, "setTimerFdTime", ret);
+  }
+}
+
+static void throwException(JNIEnv *env, int error, const char *action,
+                           const char *iface) {
+  const std::string &msg = "Error: " + std::string(action) + " " +
+                           std::string(iface) + ": " +
+                           std::string(strerror(error));
+  jniThrowException(env, "java/lang/IllegalStateException", msg.c_str());
+}
+
+// enable or disable  carrier on tun / tap interface.
+static void setTunTapCarrierEnabledImpl(JNIEnv *env, const char *iface,
+                                        int tunFd, bool enabled) {
+  uint32_t carrierOn = enabled;
+  if (ioctl(tunFd, TUNSETCARRIER, &carrierOn)) {
+    throwException(env, errno, "set carrier", iface);
+  }
+}
+
+static int createTunTapImpl(JNIEnv *env, bool isTun, bool hasCarrier,
+                            bool setIffMulticast, const char *iface) {
+  base::unique_fd tun(open("/dev/tun", O_RDWR | O_NONBLOCK));
+  ifreq ifr{};
+
+  // Allocate interface.
+  ifr.ifr_flags = (isTun ? IFF_TUN : IFF_TAP) | IFF_NO_PI;
+  if (!hasCarrier) {
+    // Using IFF_NO_CARRIER is supported starting in kernel version >= 6.0
+    // Up until then, unsupported flags are ignored.
+    if (!bpf::isAtLeastKernelVersion(6, 0, 0)) {
+      throwException(env, EOPNOTSUPP, "IFF_NO_CARRIER not supported",
+                     ifr.ifr_name);
+      return -1;
+    }
+    ifr.ifr_flags |= IFF_NO_CARRIER;
+  }
+  strlcpy(ifr.ifr_name, iface, IFNAMSIZ);
+  if (ioctl(tun.get(), TUNSETIFF, &ifr)) {
+    throwException(env, errno, "allocating", ifr.ifr_name);
+    return -1;
+  }
+
+  // Mark some TAP interfaces as supporting multicast
+  if (setIffMulticast && !isTun) {
+    base::unique_fd inet6CtrlSock(socket(AF_INET6, SOCK_DGRAM, 0));
+    ifr.ifr_flags = IFF_MULTICAST;
+
+    if (ioctl(inet6CtrlSock.get(), SIOCSIFFLAGS, &ifr)) {
+      throwException(env, errno, "set IFF_MULTICAST", ifr.ifr_name);
+      return -1;
+    }
+  }
+
+  return tun.release();
+}
+
+static void bringUpInterfaceImpl(JNIEnv *env, const char *iface) {
+  // Activate interface using an unconnected datagram socket.
+  base::unique_fd inet6CtrlSock(socket(AF_INET6, SOCK_DGRAM, 0));
+
+  ifreq ifr{};
+  strlcpy(ifr.ifr_name, iface, IFNAMSIZ);
+  if (ioctl(inet6CtrlSock.get(), SIOCGIFFLAGS, &ifr)) {
+    throwException(env, errno, "read flags", iface);
+    return;
+  }
+  ifr.ifr_flags |= IFF_UP;
+  if (ioctl(inet6CtrlSock.get(), SIOCSIFFLAGS, &ifr)) {
+    throwException(env, errno, "set IFF_UP", iface);
+    return;
+  }
+}
+
+//------------------------------------------------------------------------------
+
+static void setTunTapCarrierEnabled(JNIEnv *env, jclass /* clazz */,
+                                    jstring jIface, jint tunFd,
+                                    jboolean enabled) {
+  ScopedUtfChars iface(env, jIface);
+  if (!iface.c_str()) {
+    jniThrowNullPointerException(env, "iface");
+    return;
+  }
+  setTunTapCarrierEnabledImpl(env, iface.c_str(), tunFd, enabled);
+}
+
+static jint createTunTap(JNIEnv *env, jclass /* clazz */, jboolean isTun,
+                         jboolean hasCarrier, jboolean setIffMulticast,
+                         jstring jIface) {
+  ScopedUtfChars iface(env, jIface);
+  if (!iface.c_str()) {
+    jniThrowNullPointerException(env, "iface");
+    return -1;
+  }
+
+  return createTunTapImpl(env, isTun, hasCarrier, setIffMulticast,
+                          iface.c_str());
+}
+
+static void bringUpInterface(JNIEnv *env, jclass /* clazz */, jstring jIface) {
+  ScopedUtfChars iface(env, jIface);
+  if (!iface.c_str()) {
+    jniThrowNullPointerException(env, "iface");
+    return;
+  }
+  bringUpInterfaceImpl(env, iface.c_str());
+}
+
+//------------------------------------------------------------------------------
+
+/*
+ * JNI registration.
+ */
+static const JNINativeMethod gMethods[] = {
+    /* name, signature, funcPtr */
+    {"createTimerFd", "()I", (void *)createTimerFd},
+    {"setTimerFdTime", "(IJ)V", (void *)setTimerFdTime},
+    {"setTunTapCarrierEnabled", "(Ljava/lang/String;IZ)V",
+     (void *)setTunTapCarrierEnabled},
+    {"createTunTap", "(ZZZLjava/lang/String;)I", (void *)createTunTap},
+    {"bringUpInterface", "(Ljava/lang/String;)V", (void *)bringUpInterface},
+};
+
+int register_com_android_net_module_util_ServiceConnectivityJni(
+    JNIEnv *env, char const *class_name) {
+  return jniRegisterNativeMethods(env, class_name, gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/staticlibs/native/tcutils/tcutils.cpp b/staticlibs/native/tcutils/tcutils.cpp
index 21e781c..5425d0e 100644
--- a/staticlibs/native/tcutils/tcutils.cpp
+++ b/staticlibs/native/tcutils/tcutils.cpp
@@ -361,7 +361,7 @@
 const sockaddr_nl KERNEL_NLADDR = {AF_NETLINK, 0, 0, 0};
 const uint16_t NETLINK_REQUEST_FLAGS = NLM_F_REQUEST | NLM_F_ACK;
 
-int sendAndProcessNetlinkResponse(const void *req, int len) {
+int sendAndProcessNetlinkResponse(const void *req, int len, bool enoent_ok) {
   // TODO: use unique_fd instead of ScopeGuard
   unique_fd fd(socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE));
   if (!fd.ok()) {
@@ -445,7 +445,9 @@
     return -ENOMSG;
   }
 
-  if (resp.e.error) {
+  if (resp.e.error == -ENOENT) {
+    if (!enoent_ok) ALOGE("NLMSG_ERROR message returned ENOENT");
+  } else if (resp.e.error) {
     ALOGE("NLMSG_ERROR message return error: %d", resp.e.error);
   }
   return resp.e.error; // returns 0 on success
@@ -560,7 +562,8 @@
   };
 #undef CLSACT
 
-  return sendAndProcessNetlinkResponse(&req, sizeof(req));
+  const bool enoent_ok = (nlMsgType == RTM_DELQDISC);
+  return sendAndProcessNetlinkResponse(&req, sizeof(req), enoent_ok);
 }
 
 // tc filter add dev .. in/egress prio 1 protocol ipv6/ip bpf object-pinned
@@ -666,7 +669,7 @@
   snprintf(req.options.name.str, sizeof(req.options.name.str), "%s:[*fsobj]",
            basename(bpfProgPath));
 
-  int error = sendAndProcessNetlinkResponse(&req, sizeof(req));
+  int error = sendAndProcessNetlinkResponse(&req, sizeof(req), false);
   return error;
 }
 
@@ -698,7 +701,8 @@
     return error;
   }
   return sendAndProcessNetlinkResponse(filter.getRequest(),
-                                       filter.getRequestSize());
+                                       filter.getRequestSize(),
+                                       false);
 }
 
 // tc filter del dev .. in/egress prio .. protocol ..
@@ -726,7 +730,7 @@
           },
   };
 
-  return sendAndProcessNetlinkResponse(&req, sizeof(req));
+  return sendAndProcessNetlinkResponse(&req, sizeof(req), true);
 }
 
 } // namespace android
diff --git a/staticlibs/native/timerfdutils/com_android_net_module_util_TimerFdUtils.cpp b/staticlibs/native/timerfdutils/com_android_net_module_util_TimerFdUtils.cpp
deleted file mode 100644
index c4c960d..0000000
--- a/staticlibs/native/timerfdutils/com_android_net_module_util_TimerFdUtils.cpp
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include <errno.h>
-#include <jni.h>
-#include <nativehelper/JNIHelp.h>
-#include <nativehelper/scoped_utf_chars.h>
-#include <stdint.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/epoll.h>
-#include <sys/timerfd.h>
-#include <time.h>
-#include <unistd.h>
-
-#define MSEC_PER_SEC 1000
-#define NSEC_PER_MSEC 1000000
-
-namespace android {
-
-static jint
-com_android_net_module_util_TimerFdUtils_createTimerFd(JNIEnv *env,
-                                                       jclass clazz) {
-  int tfd;
-  tfd = timerfd_create(CLOCK_BOOTTIME, 0);
-  if (tfd == -1) {
-    jniThrowErrnoException(env, "createTimerFd", tfd);
-  }
-  return tfd;
-}
-
-static void
-com_android_net_module_util_TimerFdUtils_setTime(JNIEnv *env, jclass clazz,
-                                                 jint tfd, jlong milliseconds) {
-  struct itimerspec new_value;
-  new_value.it_value.tv_sec = milliseconds / MSEC_PER_SEC;
-  new_value.it_value.tv_nsec = (milliseconds % MSEC_PER_SEC) * NSEC_PER_MSEC;
-  // Set the interval time to 0 because it's designed for repeated timer expirations after the
-  // initial expiration, which doesn't fit the current usage.
-  new_value.it_interval.tv_sec = 0;
-  new_value.it_interval.tv_nsec = 0;
-
-  int ret = timerfd_settime(tfd, 0, &new_value, NULL);
-  if (ret == -1) {
-    jniThrowErrnoException(env, "setTime", ret);
-  }
-}
-
-/*
- * JNI registration.
- */
-static const JNINativeMethod gMethods[] = {
-    /* name, signature, funcPtr */
-    {"createTimerFd", "()I",
-     (void *)com_android_net_module_util_TimerFdUtils_createTimerFd},
-    {"setTime", "(IJ)V",
-     (void *)com_android_net_module_util_TimerFdUtils_setTime},
-};
-
-int register_com_android_net_module_util_TimerFdUtils(JNIEnv *env,
-                                                      char const *class_name) {
-  return jniRegisterNativeMethods(env, class_name, gMethods, NELEM(gMethods));
-}
-
-}; // namespace android
diff --git a/staticlibs/netd/Android.bp b/staticlibs/netd/Android.bp
index 44abba2..03f5f06 100644
--- a/staticlibs/netd/Android.bp
+++ b/staticlibs/netd/Android.bp
@@ -22,7 +22,7 @@
     sdk_version: "system_current",
     min_sdk_version: "30",
     static_libs: [
-        "netd_aidl_interface-V15-java",
+        "netd_aidl_interface-V16-java",
     ],
     apex_available: [
         "//apex_available:platform", // used from services.net
@@ -45,7 +45,7 @@
 cc_library_static {
     name: "netd_aidl_interface-lateststable-ndk",
     whole_static_libs: [
-        "netd_aidl_interface-V15-ndk",
+        "netd_aidl_interface-V16-ndk",
     ],
     apex_available: [
         "com.android.resolv",
@@ -56,12 +56,12 @@
 
 cc_defaults {
     name: "netd_aidl_interface_lateststable_cpp_static",
-    static_libs: ["netd_aidl_interface-V15-cpp"],
+    static_libs: ["netd_aidl_interface-V16-cpp"],
 }
 
 cc_defaults {
     name: "netd_aidl_interface_lateststable_cpp_shared",
-    shared_libs: ["netd_aidl_interface-V15-cpp"],
+    shared_libs: ["netd_aidl_interface-V16-cpp"],
 }
 
 aidl_interface {
@@ -171,6 +171,10 @@
             version: "15",
             imports: [],
         },
+        {
+            version: "16",
+            imports: [],
+        },
 
     ],
     frozen: true,
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/.hash b/staticlibs/netd/aidl_api/netd_aidl_interface/16/.hash
new file mode 100644
index 0000000..08cd338
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/.hash
@@ -0,0 +1 @@
+28e20632b92e146787d32437a53aaa5ad39125b7
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/INetd.aidl
new file mode 100644
index 0000000..8351b56
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/INetd.aidl
@@ -0,0 +1,272 @@
+/**
+ * Copyright (c) 2016, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetd {
+  boolean isAlive();
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  boolean firewallReplaceUidChain(in @utf8InCpp String chainName, boolean isAllowlist, in int[] uids);
+  boolean bandwidthEnableDataSaver(boolean enable);
+  /**
+   * @deprecated use networkCreate() instead.
+   */
+  void networkCreatePhysical(int netId, int permission);
+  /**
+   * @deprecated use networkCreate() instead.
+   */
+  void networkCreateVpn(int netId, boolean secure);
+  void networkDestroy(int netId);
+  void networkAddInterface(int netId, in @utf8InCpp String iface);
+  void networkRemoveInterface(int netId, in @utf8InCpp String iface);
+  void networkAddUidRanges(int netId, in android.net.UidRangeParcel[] uidRanges);
+  void networkRemoveUidRanges(int netId, in android.net.UidRangeParcel[] uidRanges);
+  void networkRejectNonSecureVpn(boolean add, in android.net.UidRangeParcel[] uidRanges);
+  void socketDestroy(in android.net.UidRangeParcel[] uidRanges, in int[] exemptUids);
+  boolean tetherApplyDnsInterfaces();
+  android.net.TetherStatsParcel[] tetherGetStats();
+  void interfaceAddAddress(in @utf8InCpp String ifName, in @utf8InCpp String addrString, int prefixLength);
+  void interfaceDelAddress(in @utf8InCpp String ifName, in @utf8InCpp String addrString, int prefixLength);
+  @utf8InCpp String getProcSysNet(int ipversion, int which, in @utf8InCpp String ifname, in @utf8InCpp String parameter);
+  void setProcSysNet(int ipversion, int which, in @utf8InCpp String ifname, in @utf8InCpp String parameter, in @utf8InCpp String value);
+  void ipSecSetEncapSocketOwner(in ParcelFileDescriptor socket, int newUid);
+  int ipSecAllocateSpi(int transformId, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int spi);
+  void ipSecAddSecurityAssociation(int transformId, int mode, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int underlyingNetId, int spi, int markValue, int markMask, in @utf8InCpp String authAlgo, in byte[] authKey, in int authTruncBits, in @utf8InCpp String cryptAlgo, in byte[] cryptKey, in int cryptTruncBits, in @utf8InCpp String aeadAlgo, in byte[] aeadKey, in int aeadIcvBits, int encapType, int encapLocalPort, int encapRemotePort, int interfaceId);
+  void ipSecDeleteSecurityAssociation(int transformId, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int spi, int markValue, int markMask, int interfaceId);
+  void ipSecApplyTransportModeTransform(in ParcelFileDescriptor socket, int transformId, int direction, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int spi);
+  void ipSecRemoveTransportModeTransform(in ParcelFileDescriptor socket);
+  void ipSecAddSecurityPolicy(int transformId, int selAddrFamily, int direction, in @utf8InCpp String tmplSrcAddress, in @utf8InCpp String tmplDstAddress, int spi, int markValue, int markMask, int interfaceId);
+  void ipSecUpdateSecurityPolicy(int transformId, int selAddrFamily, int direction, in @utf8InCpp String tmplSrcAddress, in @utf8InCpp String tmplDstAddress, int spi, int markValue, int markMask, int interfaceId);
+  void ipSecDeleteSecurityPolicy(int transformId, int selAddrFamily, int direction, int markValue, int markMask, int interfaceId);
+  void ipSecAddTunnelInterface(in @utf8InCpp String deviceName, in @utf8InCpp String localAddress, in @utf8InCpp String remoteAddress, int iKey, int oKey, int interfaceId);
+  void ipSecUpdateTunnelInterface(in @utf8InCpp String deviceName, in @utf8InCpp String localAddress, in @utf8InCpp String remoteAddress, int iKey, int oKey, int interfaceId);
+  void ipSecRemoveTunnelInterface(in @utf8InCpp String deviceName);
+  void wakeupAddInterface(in @utf8InCpp String ifName, in @utf8InCpp String prefix, int mark, int mask);
+  void wakeupDelInterface(in @utf8InCpp String ifName, in @utf8InCpp String prefix, int mark, int mask);
+  void setIPv6AddrGenMode(in @utf8InCpp String ifName, int mode);
+  void idletimerAddInterface(in @utf8InCpp String ifName, int timeout, in @utf8InCpp String classLabel);
+  void idletimerRemoveInterface(in @utf8InCpp String ifName, int timeout, in @utf8InCpp String classLabel);
+  void strictUidCleartextPenalty(int uid, int policyPenalty);
+  /**
+   * @deprecated This method has no effect and throws UnsupportedOperationException. The clatd control plane moved to the mainline module starting in T. See ClatCoordinator.
+   */
+  @utf8InCpp String clatdStart(in @utf8InCpp String ifName, in @utf8InCpp String nat64Prefix);
+  /**
+   * @deprecated This method has no effect and throws UnsupportedOperationException. The clatd control plane moved to the mainline module starting in T. See ClatCoordinator.
+   */
+  void clatdStop(in @utf8InCpp String ifName);
+  boolean ipfwdEnabled();
+  @utf8InCpp String[] ipfwdGetRequesterList();
+  void ipfwdEnableForwarding(in @utf8InCpp String requester);
+  void ipfwdDisableForwarding(in @utf8InCpp String requester);
+  void ipfwdAddInterfaceForward(in @utf8InCpp String fromIface, in @utf8InCpp String toIface);
+  void ipfwdRemoveInterfaceForward(in @utf8InCpp String fromIface, in @utf8InCpp String toIface);
+  void bandwidthSetInterfaceQuota(in @utf8InCpp String ifName, long bytes);
+  void bandwidthRemoveInterfaceQuota(in @utf8InCpp String ifName);
+  void bandwidthSetInterfaceAlert(in @utf8InCpp String ifName, long bytes);
+  void bandwidthRemoveInterfaceAlert(in @utf8InCpp String ifName);
+  void bandwidthSetGlobalAlert(long bytes);
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  void bandwidthAddNaughtyApp(int uid);
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  void bandwidthRemoveNaughtyApp(int uid);
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  void bandwidthAddNiceApp(int uid);
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  void bandwidthRemoveNiceApp(int uid);
+  void tetherStart(in @utf8InCpp String[] dhcpRanges);
+  void tetherStop();
+  boolean tetherIsEnabled();
+  void tetherInterfaceAdd(in @utf8InCpp String ifName);
+  void tetherInterfaceRemove(in @utf8InCpp String ifName);
+  @utf8InCpp String[] tetherInterfaceList();
+  void tetherDnsSet(int netId, in @utf8InCpp String[] dnsAddrs);
+  @utf8InCpp String[] tetherDnsList();
+  void networkAddRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop);
+  void networkRemoveRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop);
+  void networkAddLegacyRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop, int uid);
+  void networkRemoveLegacyRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop, int uid);
+  int networkGetDefault();
+  void networkSetDefault(int netId);
+  void networkClearDefault();
+  void networkSetPermissionForNetwork(int netId, int permission);
+  void networkSetPermissionForUser(int permission, in int[] uids);
+  void networkClearPermissionForUser(in int[] uids);
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  void trafficSetNetPermForUids(int permission, in int[] uids);
+  void networkSetProtectAllow(int uid);
+  void networkSetProtectDeny(int uid);
+  boolean networkCanProtect(int uid);
+  void firewallSetFirewallType(int firewalltype);
+  void firewallSetInterfaceRule(in @utf8InCpp String ifName, int firewallRule);
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  void firewallSetUidRule(int childChain, int uid, int firewallRule);
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  void firewallEnableChildChain(int childChain, boolean enable);
+  @utf8InCpp String[] interfaceGetList();
+  android.net.InterfaceConfigurationParcel interfaceGetCfg(in @utf8InCpp String ifName);
+  void interfaceSetCfg(in android.net.InterfaceConfigurationParcel cfg);
+  void interfaceSetIPv6PrivacyExtensions(in @utf8InCpp String ifName, boolean enable);
+  void interfaceClearAddrs(in @utf8InCpp String ifName);
+  void interfaceSetEnableIPv6(in @utf8InCpp String ifName, boolean enable);
+  void interfaceSetMtu(in @utf8InCpp String ifName, int mtu);
+  void tetherAddForward(in @utf8InCpp String intIface, in @utf8InCpp String extIface);
+  void tetherRemoveForward(in @utf8InCpp String intIface, in @utf8InCpp String extIface);
+  void setTcpRWmemorySize(in @utf8InCpp String rmemValues, in @utf8InCpp String wmemValues);
+  void registerUnsolicitedEventListener(android.net.INetdUnsolicitedEventListener listener);
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  void firewallAddUidInterfaceRules(in @utf8InCpp String ifName, in int[] uids);
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  void firewallRemoveUidInterfaceRules(in int[] uids);
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  void trafficSwapActiveStatsMap();
+  IBinder getOemNetd();
+  void tetherStartWithConfiguration(in android.net.TetherConfigParcel config);
+  android.net.MarkMaskParcel getFwmarkForNetwork(int netId);
+  void networkAddRouteParcel(int netId, in android.net.RouteInfoParcel routeInfo);
+  void networkUpdateRouteParcel(int netId, in android.net.RouteInfoParcel routeInfo);
+  void networkRemoveRouteParcel(int netId, in android.net.RouteInfoParcel routeInfo);
+  /**
+   * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+   */
+  void tetherOffloadRuleAdd(in android.net.TetherOffloadRuleParcel rule);
+  /**
+   * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+   */
+  void tetherOffloadRuleRemove(in android.net.TetherOffloadRuleParcel rule);
+  /**
+   * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+   */
+  android.net.TetherStatsParcel[] tetherOffloadGetStats();
+  /**
+   * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+   */
+  void tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes);
+  /**
+   * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+   */
+  android.net.TetherStatsParcel tetherOffloadGetAndClearStats(int ifIndex);
+  void networkCreate(in android.net.NativeNetworkConfig config);
+  void networkAddUidRangesParcel(in android.net.netd.aidl.NativeUidRangeConfig uidRangesConfig);
+  void networkRemoveUidRangesParcel(in android.net.netd.aidl.NativeUidRangeConfig uidRangesConfig);
+  void ipSecMigrate(in android.net.IpSecMigrateInfoParcel migrateInfo);
+  void setNetworkAllowlist(in android.net.netd.aidl.NativeUidRangeConfig[] allowedNetworks);
+  void networkAllowBypassVpnOnNetwork(boolean allow, int uid, int netId);
+  const int IPV4 = 4;
+  const int IPV6 = 6;
+  const int CONF = 1;
+  const int NEIGH = 2;
+  const String IPSEC_INTERFACE_PREFIX = "ipsec";
+  const int IPV6_ADDR_GEN_MODE_EUI64 = 0;
+  const int IPV6_ADDR_GEN_MODE_NONE = 1;
+  const int IPV6_ADDR_GEN_MODE_STABLE_PRIVACY = 2;
+  const int IPV6_ADDR_GEN_MODE_RANDOM = 3;
+  const int IPV6_ADDR_GEN_MODE_DEFAULT = 0;
+  const int PENALTY_POLICY_ACCEPT = 1;
+  const int PENALTY_POLICY_LOG = 2;
+  const int PENALTY_POLICY_REJECT = 3;
+  const int CLAT_MARK = 0xdeadc1a7;
+  const int LOCAL_NET_ID = 99;
+  const int DUMMY_NET_ID = 51;
+  const int UNREACHABLE_NET_ID = 52;
+  const String NEXTHOP_NONE = "";
+  const String NEXTHOP_UNREACHABLE = "unreachable";
+  const String NEXTHOP_THROW = "throw";
+  const int PERMISSION_NONE = 0;
+  const int PERMISSION_NETWORK = 1;
+  const int PERMISSION_SYSTEM = 2;
+  /**
+   * @deprecated usage is internal to module.
+   */
+  const int NO_PERMISSIONS = 0;
+  /**
+   * @deprecated usage is internal to module.
+   */
+  const int PERMISSION_INTERNET = 4;
+  /**
+   * @deprecated usage is internal to module.
+   */
+  const int PERMISSION_UPDATE_DEVICE_STATS = 8;
+  /**
+   * @deprecated usage is internal to module.
+   */
+  const int PERMISSION_UNINSTALLED = (-1) /* -1 */;
+  /**
+   * @deprecated use FIREWALL_ALLOWLIST.
+   */
+  const int FIREWALL_WHITELIST = 0;
+  const int FIREWALL_ALLOWLIST = 0;
+  /**
+   * @deprecated use FIREWALL_DENYLIST.
+   */
+  const int FIREWALL_BLACKLIST = 1;
+  const int FIREWALL_DENYLIST = 1;
+  const int FIREWALL_RULE_ALLOW = 1;
+  const int FIREWALL_RULE_DENY = 2;
+  const int FIREWALL_CHAIN_NONE = 0;
+  const int FIREWALL_CHAIN_DOZABLE = 1;
+  const int FIREWALL_CHAIN_STANDBY = 2;
+  const int FIREWALL_CHAIN_POWERSAVE = 3;
+  const int FIREWALL_CHAIN_RESTRICTED = 4;
+  const String IF_STATE_UP = "up";
+  const String IF_STATE_DOWN = "down";
+  const String IF_FLAG_BROADCAST = "broadcast";
+  const String IF_FLAG_LOOPBACK = "loopback";
+  const String IF_FLAG_POINTOPOINT = "point-to-point";
+  const String IF_FLAG_RUNNING = "running";
+  const String IF_FLAG_MULTICAST = "multicast";
+  const int IPSEC_DIRECTION_IN = 0;
+  const int IPSEC_DIRECTION_OUT = 1;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/INetdUnsolicitedEventListener.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/INetdUnsolicitedEventListener.aidl
new file mode 100644
index 0000000..31775df
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/INetdUnsolicitedEventListener.aidl
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2018, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetdUnsolicitedEventListener {
+  oneway void onInterfaceClassActivityChanged(boolean isActive, int timerLabel, long timestampNs, int uid);
+  oneway void onQuotaLimitReached(@utf8InCpp String alertName, @utf8InCpp String ifName);
+  oneway void onInterfaceDnsServerInfo(@utf8InCpp String ifName, long lifetimeS, in @utf8InCpp String[] servers);
+  oneway void onInterfaceAddressUpdated(@utf8InCpp String addr, @utf8InCpp String ifName, int flags, int scope);
+  oneway void onInterfaceAddressRemoved(@utf8InCpp String addr, @utf8InCpp String ifName, int flags, int scope);
+  oneway void onInterfaceAdded(@utf8InCpp String ifName);
+  oneway void onInterfaceRemoved(@utf8InCpp String ifName);
+  oneway void onInterfaceChanged(@utf8InCpp String ifName, boolean up);
+  oneway void onInterfaceLinkStateChanged(@utf8InCpp String ifName, boolean up);
+  oneway void onRouteChanged(boolean updated, @utf8InCpp String route, @utf8InCpp String gateway, @utf8InCpp String ifName);
+  oneway void onStrictCleartextDetected(int uid, @utf8InCpp String hex);
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/InterfaceConfigurationParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/InterfaceConfigurationParcel.aidl
new file mode 100644
index 0000000..1869d8d
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/InterfaceConfigurationParcel.aidl
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable InterfaceConfigurationParcel {
+  @utf8InCpp String ifName;
+  @utf8InCpp String hwAddr;
+  @utf8InCpp String ipv4Addr;
+  int prefixLength;
+  @utf8InCpp String[] flags;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/IpSecMigrateInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/IpSecMigrateInfoParcel.aidl
new file mode 100644
index 0000000..975a261
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/IpSecMigrateInfoParcel.aidl
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaOnlyImmutable
+parcelable IpSecMigrateInfoParcel {
+  int requestId;
+  int selAddrFamily;
+  int direction;
+  @utf8InCpp String oldSourceAddress;
+  @utf8InCpp String oldDestinationAddress;
+  @utf8InCpp String newSourceAddress;
+  @utf8InCpp String newDestinationAddress;
+  int interfaceId;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/MarkMaskParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/MarkMaskParcel.aidl
new file mode 100644
index 0000000..8ea20d1
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/MarkMaskParcel.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable MarkMaskParcel {
+  int mark;
+  int mask;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeNetworkConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeNetworkConfig.aidl
new file mode 100644
index 0000000..77d814b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeNetworkConfig.aidl
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable NativeNetworkConfig {
+  int netId;
+  android.net.NativeNetworkType networkType = android.net.NativeNetworkType.PHYSICAL;
+  int permission;
+  boolean secure;
+  android.net.NativeVpnType vpnType = android.net.NativeVpnType.PLATFORM;
+  boolean excludeLocalRoutes = false;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeNetworkType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeNetworkType.aidl
new file mode 100644
index 0000000..e77a143
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeNetworkType.aidl
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@Backing(type="int")
+enum NativeNetworkType {
+  PHYSICAL = 0,
+  VIRTUAL = 1,
+  PHYSICAL_LOCAL = 2,
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeVpnType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeVpnType.aidl
new file mode 100644
index 0000000..8a8be83
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeVpnType.aidl
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@Backing(type="int")
+enum NativeVpnType {
+  SERVICE = 1,
+  PLATFORM = 2,
+  LEGACY = 3,
+  OEM = 4,
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/RouteInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/RouteInfoParcel.aidl
new file mode 100644
index 0000000..5ef95e6
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/RouteInfoParcel.aidl
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+parcelable RouteInfoParcel {
+  @utf8InCpp String destination;
+  @utf8InCpp String ifName;
+  @utf8InCpp String nextHop;
+  int mtu;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherConfigParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherConfigParcel.aidl
new file mode 100644
index 0000000..7b39c22
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherConfigParcel.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable TetherConfigParcel {
+  boolean usingLegacyDnsProxy;
+  @utf8InCpp String[] dhcpRanges;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherOffloadRuleParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherOffloadRuleParcel.aidl
new file mode 100644
index 0000000..983e986
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherOffloadRuleParcel.aidl
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable TetherOffloadRuleParcel {
+  int inputInterfaceIndex;
+  int outputInterfaceIndex;
+  byte[] destination;
+  int prefixLength;
+  byte[] srcL2Address;
+  byte[] dstL2Address;
+  int pmtu = 1500;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherStatsParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherStatsParcel.aidl
new file mode 100644
index 0000000..5f1b722
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherStatsParcel.aidl
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable TetherStatsParcel {
+  @utf8InCpp String iface;
+  long rxBytes;
+  long rxPackets;
+  long txBytes;
+  long txPackets;
+  int ifIndex = 0;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/UidRangeParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/UidRangeParcel.aidl
new file mode 100644
index 0000000..72e987a
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/UidRangeParcel.aidl
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable UidRangeParcel {
+  int start;
+  int stop;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/netd/aidl/NativeUidRangeConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/netd/aidl/NativeUidRangeConfig.aidl
new file mode 100644
index 0000000..9bb679f
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/netd/aidl/NativeUidRangeConfig.aidl
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.netd.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable NativeUidRangeConfig {
+  int netId;
+  android.net.UidRangeParcel[] uidRanges;
+  int subPriority;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl
index 80b3b62..8351b56 100644
--- a/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl
@@ -227,9 +227,21 @@
   const int PERMISSION_NONE = 0;
   const int PERMISSION_NETWORK = 1;
   const int PERMISSION_SYSTEM = 2;
+  /**
+   * @deprecated usage is internal to module.
+   */
   const int NO_PERMISSIONS = 0;
+  /**
+   * @deprecated usage is internal to module.
+   */
   const int PERMISSION_INTERNET = 4;
+  /**
+   * @deprecated usage is internal to module.
+   */
   const int PERMISSION_UPDATE_DEVICE_STATS = 8;
+  /**
+   * @deprecated usage is internal to module.
+   */
   const int PERMISSION_UNINSTALLED = (-1) /* -1 */;
   /**
    * @deprecated use FIREWALL_ALLOWLIST.
diff --git a/staticlibs/netd/binder/android/net/INetd.aidl b/staticlibs/netd/binder/android/net/INetd.aidl
index e4c63b9..be8f538 100644
--- a/staticlibs/netd/binder/android/net/INetd.aidl
+++ b/staticlibs/netd/binder/android/net/INetd.aidl
@@ -933,24 +933,27 @@
    /**
     * NO_PERMISSIONS indicates that this app is installed and doesn't have either
     * PERMISSION_INTERNET or PERMISSION_UPDATE_DEVICE_STATS.
-    * TODO: use PERMISSION_NONE to represent this case
+    * @deprecated usage is internal to module.
     */
     const int NO_PERMISSIONS = 0;
 
    /**
-    * PERMISSION_INTERNET indicates that the app can create AF_INET and AF_INET6 sockets
+    * PERMISSION_INTERNET indicates that the app can create AF_INET and AF_INET6 sockets.
+    * @deprecated usage is internal to module.
     */
     const int PERMISSION_INTERNET = 4;
 
    /**
     * PERMISSION_UPDATE_DEVICE_STATS is used for system UIDs and privileged apps
-    * that have the UPDATE_DEVICE_STATS permission
+    * that have the UPDATE_DEVICE_STATS permission.
+    * @deprecated usage is internal to module.
     */
     const int PERMISSION_UPDATE_DEVICE_STATS = 8;
 
    /**
     * PERMISSION_UNINSTALLED is used when an app is uninstalled from the device. All internet
-    * related permissions need to be cleaned
+    * related permissions need to be cleaned.
+    * @deprecated usage is internal to module.
     */
     const int PERMISSION_UNINSTALLED = -1;
 
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index 8c54e6a..f4f1ea9 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -28,6 +28,7 @@
         "net-utils-device-common-struct-base",
         "net-utils-device-common-wear",
         "net-utils-service-connectivity",
+        "truth",
     ],
     libs: [
         "android.test.runner.stubs",
@@ -55,6 +56,7 @@
         // For mockito extended
         "libdexmakerjvmtiagent",
         "libstaticjvmtiagent",
+        "libcom_android_net_moduletests_util_jni",
     ],
     jarjar_rules: "jarjar-rules.txt",
     test_suites: ["device-tests"],
diff --git a/staticlibs/native/timerfdutils/Android.bp b/staticlibs/tests/unit/jni/Android.bp
similarity index 66%
copy from staticlibs/native/timerfdutils/Android.bp
copy to staticlibs/tests/unit/jni/Android.bp
index 939a2d2..c444159 100644
--- a/staticlibs/native/timerfdutils/Android.bp
+++ b/staticlibs/tests/unit/jni/Android.bp
@@ -14,33 +14,26 @@
 
 package {
     default_team: "trendy_team_fwk_core_networking",
+    // See: http://go/android-license-faq
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-cc_library_static {
-    name: "libnet_utils_device_common_timerfdjni",
-    srcs: [
-        "com_android_net_module_util_TimerFdUtils.cpp",
-    ],
-    header_libs: [
-        "jni_headers",
-    ],
-    shared_libs: [
-        "liblog",
-        "libnativehelper_compat_libc++",
-    ],
+cc_library_shared {
+    name: "libcom_android_net_moduletests_util_jni",
     cflags: [
         "-Wall",
         "-Werror",
         "-Wno-unused-parameter",
+        "-Wthread-safety",
     ],
-    sdk_version: "current",
-    min_sdk_version: "30",
-    apex_available: [
-        "com.android.tethering",
-        "//apex_available:platform",
+    srcs: [
+        "com_android_net_moduletests_util/onload.cpp",
     ],
-    visibility: [
-        "//packages/modules/Connectivity:__subpackages__",
+    static_libs: [
+        "libserviceconnectivityjni",
+    ],
+    shared_libs: [
+        "liblog",
+        "libnativehelper",
     ],
 }
diff --git a/staticlibs/tests/unit/jni/com_android_net_moduletests_util/onload.cpp b/staticlibs/tests/unit/jni/com_android_net_moduletests_util/onload.cpp
new file mode 100644
index 0000000..af4810f
--- /dev/null
+++ b/staticlibs/tests/unit/jni/com_android_net_moduletests_util/onload.cpp
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "jni.h"
+#include <nativehelper/JNIHelp.h>
+
+#define LOG_TAG "NetworkStaticLibTestsJni"
+#include <android/log.h>
+
+namespace android {
+
+int register_com_android_net_module_util_ServiceConnectivityJni(JNIEnv *env,
+                                                      char const *class_name);
+
+extern "C" jint JNI_OnLoad(JavaVM *vm, void *) {
+  JNIEnv *env;
+  if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
+    __android_log_print(ANDROID_LOG_FATAL, LOG_TAG, "ERROR: GetEnv failed");
+    return JNI_ERR;
+  }
+
+  if (register_com_android_net_module_util_ServiceConnectivityJni(
+          env, "com/android/net/moduletests/util/ServiceConnectivityJni") < 0)
+    return JNI_ERR;
+
+  return JNI_VERSION_1_6;
+}
+
+}; // namespace android
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt
index 7244803..1aa943e 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt
@@ -19,7 +19,7 @@
 import android.util.SparseArray
 import androidx.test.filters.SmallTest
 import androidx.test.runner.AndroidJUnit4
-import com.android.testutils.assertThrows
+import kotlin.test.assertContentEquals
 import org.junit.Test
 import org.junit.runner.RunWith
 import kotlin.test.assertEquals
@@ -196,4 +196,88 @@
         assertEquals(1, CollectionUtils.getIndexForValue(sparseArray, value1Copy))
         assertEquals(2, CollectionUtils.getIndexForValue(sparseArray, value2))
     }
+
+    @Test
+    fun testConcatEmptyByteArrays() {
+        assertContentEquals(
+                byteArrayOf(),
+                CollectionUtils.concatArrays(byteArrayOf(), byteArrayOf())
+        )
+    }
+
+    @Test
+    fun testConcatEmptyStringArrays() {
+        assertContentEquals(
+                arrayOf<String>(),
+                CollectionUtils.concatArrays(
+                        String::class.java,
+                        arrayOf<String>(),
+                        arrayOf<String>()
+                )
+        )
+    }
+
+    @Test
+    fun testConcatByteArrays() {
+        val byteArr1 = byteArrayOf(1, 2, 3)
+        val byteArr2 = byteArrayOf(4, 5, 6)
+        val byteArr3 = byteArrayOf()
+        val byteArrExpected = byteArrayOf(1, 2, 3, 4, 5, 6)
+        assertContentEquals(
+                byteArrExpected,
+                CollectionUtils.concatArrays(byteArr1, byteArr2, byteArr3)
+        )
+    }
+
+    @Test
+    fun testConcatStringArrays() {
+        val stringArr1 = arrayOf("1", "2", "3")
+        val stringArr2 = arrayOf("4", "5", "6")
+        val strinvArr3 = arrayOf<String>()
+        val stringArrExpected = arrayOf("1", "2", "3", "4", "5", "6")
+        assertContentEquals(
+                stringArrExpected,
+                CollectionUtils.concatArrays(String::class.java, stringArr1, stringArr2, strinvArr3)
+        )
+    }
+
+    @Test
+    fun testPrependByteArrays() {
+        val byteArr2 = byteArrayOf(4, 5, 6)
+        val byteArrExpected = byteArrayOf(1, 2, 3, 4, 5, 6)
+        assertContentEquals(
+                byteArrExpected,
+                CollectionUtils.prependArray(byteArr2, 1, 2, 3)
+        )
+    }
+
+    @Test
+    fun testPrependStringArrays() {
+        val stringArr2 = arrayOf("4", "5", "6")
+        val stringArrExpected = arrayOf("1", "2", "3", "4", "5", "6")
+        assertContentEquals(
+                stringArrExpected,
+                CollectionUtils.prependArray(String::class.java, stringArr2, "1", "2", "3")
+        )
+    }
+
+    @Test
+    fun testAppendByteArrays() {
+        val byteArr1 = byteArrayOf(1, 2, 3)
+        val byteArrExpected = byteArrayOf(1, 2, 3, 4, 5, 6)
+        assertContentEquals(
+                byteArrExpected,
+                CollectionUtils.appendArray(byteArr1, 4, 5, 6)
+        )
+    }
+
+    @Test
+    fun testAppendStringArrays() {
+        val stringArr1 = arrayOf("1", "2", "3")
+        val stringArrExpected = arrayOf("1", "2", "3", "4", "5", "6")
+        assertContentEquals(
+                stringArrExpected,
+                CollectionUtils.appendArray(String::class.java, stringArr1, "4", "5", "6")
+        )
+    }
 }
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/HexDumpTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/HexDumpTest.java
index 5a15585..f81978a 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/HexDumpTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/HexDumpTest.java
@@ -18,6 +18,7 @@
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -50,6 +51,11 @@
     }
 
     @Test
+    public void testInvalidHexStringToByteArray() {
+        assertThrows(IllegalArgumentException.class, () -> HexDump.hexStringToByteArray("abxX"));
+    }
+
+    @Test
     public void testIntegerToByteArray() {
         assertArrayEquals(new byte[]{(byte) 0xff, (byte) 0x00, (byte) 0x00, (byte) 0x04},
                 HexDump.toByteArray((int) 0xff000004));
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/LruCacheWithExpiryTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/LruCacheWithExpiryTest.kt
new file mode 100644
index 0000000..b6af892
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/LruCacheWithExpiryTest.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util
+
+import com.android.testutils.DevSdkIgnoreRunner
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.function.LongSupplier
+
+@RunWith(DevSdkIgnoreRunner::class)
+class LruCacheWithExpiryTest {
+
+    companion object {
+        private const val CACHE_SIZE = 2
+        private const val EXPIRY_DURATION_MS = 1000L
+    }
+
+    private val timeSupplier = object : LongSupplier {
+        private var currentTimeMillis = 0L
+        override fun getAsLong(): Long = currentTimeMillis
+        fun advanceTime(millis: Long) { currentTimeMillis += millis }
+    }
+
+    private val cache = LruCacheWithExpiry<Int, String>(
+            timeSupplier, EXPIRY_DURATION_MS, CACHE_SIZE) { true }
+
+    @Test
+    fun testPutIfAbsent_keyNotPresent() {
+        val value = cache.putIfAbsent(1, "value1")
+        assertNull(value)
+        assertEquals("value1", cache.get(1))
+    }
+
+    @Test
+    fun testPutIfAbsent_keyPresent() {
+        cache.put(1, "value1")
+        val value = cache.putIfAbsent(1, "value2")
+        assertEquals("value1", value)
+        assertEquals("value1", cache.get(1))
+    }
+
+    @Test
+    fun testPutIfAbsent_keyPresentButExpired() {
+        cache.put(1, "value1")
+        // Advance time to expire the entry
+        timeSupplier.advanceTime(EXPIRY_DURATION_MS + 1)
+        val value = cache.putIfAbsent(1, "value2")
+        assertNull(value)
+        assertEquals("value2", cache.get(1))
+    }
+
+    @Test
+    fun testPutIfAbsent_maxSizeReached() {
+        cache.put(1, "value1")
+        cache.put(2, "value2")
+        cache.putIfAbsent(3, "value3") // This should evict the least recently used entry (1)
+        assertNull(cache.get(1))
+        assertEquals("value2", cache.get(2))
+        assertEquals("value3", cache.get(3))
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/RealtimeSchedulerTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/RealtimeSchedulerTest.kt
new file mode 100644
index 0000000..30b530f
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/RealtimeSchedulerTest.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util
+
+import android.os.Build
+import android.os.ConditionVariable
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Looper
+import android.os.Message
+import android.os.SystemClock
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.tryTest
+import com.android.testutils.visibleOnHandlerThread
+import com.google.common.collect.Range
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertEquals
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class RealtimeSchedulerTest {
+
+    private val TIMEOUT_MS = 1000L
+    private val TOLERANCE_MS = 50L
+    private class TestHandler(looper: Looper) : Handler(looper) {
+        override fun handleMessage(msg: Message) {
+            val pair = msg.obj as Pair<ConditionVariable, MutableList<Long>>
+            val cv = pair.first
+            cv.open()
+            val executionTimes = pair.second
+            executionTimes.add(SystemClock.elapsedRealtime())
+        }
+    }
+    private val thread = HandlerThread(RealtimeSchedulerTest::class.simpleName).apply { start() }
+    private val handler by lazy { TestHandler(thread.looper) }
+
+    @After
+    fun tearDown() {
+        thread.quitSafely()
+        thread.join()
+    }
+
+    @Test
+    fun testMultiplePostDelayedTasks() {
+        val scheduler = RealtimeScheduler(handler)
+        tryTest {
+            val initialTimeMs = SystemClock.elapsedRealtime()
+            val executionTimes = mutableListOf<Long>()
+            val cv = ConditionVariable()
+            handler.post {
+                scheduler.postDelayed(
+                    { executionTimes.add(SystemClock.elapsedRealtime() - initialTimeMs) }, 0)
+                scheduler.postDelayed(
+                    { executionTimes.add(SystemClock.elapsedRealtime() - initialTimeMs) }, 200)
+                val toBeRemoved = Runnable {
+                    executionTimes.add(SystemClock.elapsedRealtime() - initialTimeMs)
+                }
+                scheduler.postDelayed(toBeRemoved, 250)
+                scheduler.removeDelayedRunnable(toBeRemoved)
+                scheduler.postDelayed(
+                    { executionTimes.add(SystemClock.elapsedRealtime() - initialTimeMs) }, 100)
+                scheduler.postDelayed({
+                    executionTimes.add(SystemClock.elapsedRealtime() - initialTimeMs)
+                    cv.open() }, 300)
+            }
+            cv.block(TIMEOUT_MS)
+            assertEquals(4, executionTimes.size)
+            assertThat(executionTimes[0]).isIn(Range.closed(0L, TOLERANCE_MS))
+            assertThat(executionTimes[1]).isIn(Range.closed(100L, 100 + TOLERANCE_MS))
+            assertThat(executionTimes[2]).isIn(Range.closed(200L, 200 + TOLERANCE_MS))
+            assertThat(executionTimes[3]).isIn(Range.closed(300L, 300 + TOLERANCE_MS))
+        } cleanup {
+            visibleOnHandlerThread(handler) { scheduler.close() }
+        }
+    }
+
+    @Test
+    fun testMultipleSendDelayedMessages() {
+        val scheduler = RealtimeScheduler(handler)
+        tryTest {
+            val MSG_ID_0 = 0
+            val MSG_ID_1 = 1
+            val MSG_ID_2 = 2
+            val MSG_ID_3 = 3
+            val MSG_ID_4 = 4
+            val initialTimeMs = SystemClock.elapsedRealtime()
+            val executionTimes = mutableListOf<Long>()
+            val cv = ConditionVariable()
+            handler.post {
+                scheduler.sendDelayedMessage(
+                    Message.obtain(handler, MSG_ID_0, Pair(ConditionVariable(), executionTimes)), 0)
+                scheduler.sendDelayedMessage(
+                    Message.obtain(handler, MSG_ID_1, Pair(ConditionVariable(), executionTimes)),
+                    200)
+                scheduler.sendDelayedMessage(
+                    Message.obtain(handler, MSG_ID_4, Pair(ConditionVariable(), executionTimes)),
+                    250)
+                scheduler.removeDelayedMessage(MSG_ID_4)
+                scheduler.sendDelayedMessage(
+                    Message.obtain(handler, MSG_ID_2, Pair(ConditionVariable(), executionTimes)),
+                    100)
+                scheduler.sendDelayedMessage(
+                    Message.obtain(handler, MSG_ID_3, Pair(cv, executionTimes)),
+                    300)
+            }
+            cv.block(TIMEOUT_MS)
+            assertEquals(4, executionTimes.size)
+            assertThat(executionTimes[0] - initialTimeMs).isIn(Range.closed(0L, TOLERANCE_MS))
+            assertThat(executionTimes[1] - initialTimeMs)
+                .isIn(Range.closed(100L, 100 + TOLERANCE_MS))
+            assertThat(executionTimes[2] - initialTimeMs)
+                .isIn(Range.closed(200L, 200 + TOLERANCE_MS))
+            assertThat(executionTimes[3] - initialTimeMs)
+                .isIn(Range.closed(300L, 300 + TOLERANCE_MS))
+        } cleanup {
+            visibleOnHandlerThread(handler) { scheduler.close() }
+        }
+    }
+}
diff --git a/tests/unit/java/com/android/server/net/SkDestroyListenerTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/SkDestroyListenerTest.kt
similarity index 89%
rename from tests/unit/java/com/android/server/net/SkDestroyListenerTest.kt
rename to staticlibs/tests/unit/src/com/android/net/module/util/SkDestroyListenerTest.kt
index 18785e5..e4b47fe 100644
--- a/tests/unit/java/com/android/server/net/SkDestroyListenerTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/SkDestroyListenerTest.kt
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.server.net
+package com.android.net.module.util
 
 import android.os.Handler
 import android.os.HandlerThread
-import com.android.net.module.util.SharedLog
+import com.android.net.module.util.SkDestroyListener.makeSkDestroyListener
 import com.android.testutils.DevSdkIgnoreRunner
 import java.io.PrintWriter
 import org.junit.After
@@ -54,7 +54,7 @@
         doReturn(sharedLog).`when`(sharedLog).forSubComponent(any())
 
         val handler = Handler(handlerThread.looper)
-        val skDestroylistener = SkDestroyListener(null /* cookieTagMap */, handler, sharedLog)
+        val skDestroylistener = makeSkDestroyListener({} /* consumer */, handler, sharedLog)
         val pw = PrintWriter(System.out)
         skDestroylistener.dump(pw)
 
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/TerribleErrorLogTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/TerribleErrorLogTest.kt
new file mode 100644
index 0000000..5fd634e
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/TerribleErrorLogTest.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util
+
+import android.util.Log
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.testutils.tryTest
+import kotlin.test.assertContentEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class TerribleErrorLogTest {
+    @Test
+    fun testLogTerribleError() {
+        val wtfCaptures = mutableListOf<String>()
+        val prevHandler = Log.setWtfHandler { tag, what, system ->
+            wtfCaptures.add("$tag,${what.message}")
+        }
+        val statsLogCapture = mutableListOf<Pair<Int, Int>>()
+        val testStatsLog = object {
+            fun write(protoType: Int, errorType: Int) {
+                statsLogCapture.add(protoType to errorType)
+            }
+        }
+        tryTest {
+            TerribleErrorLog.logTerribleError(testStatsLog::write, "error", 1, 2)
+            assertContentEquals(listOf(1 to 2), statsLogCapture)
+            assertContentEquals(listOf("TerribleErrorLog,error"), wtfCaptures)
+        } cleanup {
+            Log.setWtfHandler(prevHandler)
+        }
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
index bd0e31d..13710b1 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
@@ -306,6 +306,50 @@
     }
 
     @Test
+    public void testCreateSetInterfaceFlagsMessage() {
+        final String expectedHexBytes =
+                "20000000100005006824000000000000"    // struct nlmsghdr
+                        + "00000000080000000100000001000100"; // struct ifinfomsg
+        final String interfaceName = "wlan0";
+        final int interfaceIndex = 8;
+        final int sequenceNumber = 0x2468;
+
+        when(mOsAccess.if_nametoindex(interfaceName)).thenReturn(interfaceIndex);
+
+        final RtNetlinkLinkMessage msg = RtNetlinkLinkMessage.createSetFlagsMessage(
+                interfaceName,
+                sequenceNumber,
+                mOsAccess,
+                NetlinkConstants.IFF_UP,
+                ~NetlinkConstants.IFF_LOWER_UP);
+        assertNotNull(msg);
+        final byte[] bytes = msg.pack(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        assertEquals(expectedHexBytes, HexDump.toHexString(bytes));
+    }
+
+    @Test
+    public void testCreateSetInterfaceMtuMessage() {
+        final String expectedHexBytes =
+                "280000001000050068240000000000000000000008000000"   // struct nlmsghdr
+                        + "000000000000000008000400DC050000"; // struct ifinfomsg
+        final String interfaceName = "wlan0";
+        final int interfaceIndex = 8;
+        final int sequenceNumber = 0x2468;
+        final int mtu = 1500;
+
+        when(mOsAccess.if_nametoindex(interfaceName)).thenReturn(interfaceIndex);
+
+        final RtNetlinkLinkMessage msg = RtNetlinkLinkMessage.createSetMtuMessage(
+                interfaceName,
+                sequenceNumber,
+                mtu,
+                mOsAccess);
+        assertNotNull(msg);
+        final byte[] bytes = msg.pack(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        assertEquals(expectedHexBytes, HexDump.toHexString(bytes));
+    }
+
+    @Test
     public void testToString() {
         final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWLINK_HEX);
         byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkPrefixMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkPrefixMessageTest.java
new file mode 100644
index 0000000..b1779cb
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkPrefixMessageTest.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink;
+
+import static android.system.OsConstants.NETLINK_ROUTE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.net.IpPrefix;
+import android.system.OsConstants;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.HexDump;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RtNetlinkPrefixMessageTest {
+    private static final IpPrefix TEST_PREFIX = new IpPrefix("2001:db8:1:1::/64");
+
+    // An example of the full RTM_NEWPREFIX message.
+    private static final String RTM_NEWPREFIX_HEX =
+            "3C000000340000000000000000000000"            // struct nlmsghr
+            + "0A0000002F00000003400300"                  // struct prefixmsg
+            + "1400010020010DB8000100010000000000000000"  // PREFIX_ADDRESS
+            + "0C000200803A0900008D2700";                 // PREFIX_CACHEINFO
+
+    private ByteBuffer toByteBuffer(final String hexString) {
+        return ByteBuffer.wrap(HexDump.hexStringToByteArray(hexString));
+    }
+
+    @Test
+    public void testParseRtmNewPrefix() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWPREFIX_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkPrefixMessage);
+        final RtNetlinkPrefixMessage prefixmsg = (RtNetlinkPrefixMessage) msg;
+
+        final StructNlMsgHdr hdr = prefixmsg.getHeader();
+        assertNotNull(hdr);
+        assertEquals(60, hdr.nlmsg_len);
+        assertEquals(NetlinkConstants.RTM_NEWPREFIX, hdr.nlmsg_type);
+        assertEquals(0, hdr.nlmsg_flags);
+        assertEquals(0, hdr.nlmsg_seq);
+        assertEquals(0, hdr.nlmsg_pid);
+
+        final StructPrefixMsg prefixmsgHdr = prefixmsg.getPrefixMsg();
+        assertNotNull(prefixmsgHdr);
+        assertEquals((byte) OsConstants.AF_INET6, prefixmsgHdr.prefix_family);
+        assertEquals(3, prefixmsgHdr.prefix_type);
+        assertEquals(64, prefixmsgHdr.prefix_len);
+        assertEquals(0x03, prefixmsgHdr.prefix_flags);
+        assertEquals(0x2F, prefixmsgHdr.prefix_ifindex);
+
+        assertEquals(prefixmsg.getPrefix(), TEST_PREFIX);
+        assertEquals(604800L, prefixmsg.getPreferredLifetime());
+        assertEquals(2592000L, prefixmsg.getValidLifetime());
+    }
+
+    @Test
+    public void testPackRtmNewPrefix() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWPREFIX_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkPrefixMessage);
+        final RtNetlinkPrefixMessage prefixmsg = (RtNetlinkPrefixMessage) msg;
+
+        final ByteBuffer packBuffer = ByteBuffer.allocate(60);
+        packBuffer.order(ByteOrder.LITTLE_ENDIAN);
+        prefixmsg.pack(packBuffer);
+        assertEquals(RTM_NEWPREFIX_HEX, HexDump.toHexString(packBuffer.array()));
+    }
+
+    private static final String RTM_NEWPREFIX_WITHOUT_PREFIX_ADDRESS_HEX =
+            "24000000340000000000000000000000"            // struct nlmsghr
+            + "0A0000002F00000003400300"                  // struct prefixmsg
+            + "0C000200803A0900008D2700";                 // PREFIX_CACHEINFO
+
+    @Test
+    public void testParseRtmNewPrefix_withoutPrefixAddressAttribute() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWPREFIX_WITHOUT_PREFIX_ADDRESS_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNull(msg);
+    }
+
+    private static final String RTM_NEWPREFIX_WITHOUT_PREFIX_CACHEINFO_HEX =
+            "30000000340000000000000000000000"             // struct nlmsghr
+            + "0A0000002F00000003400300"                   // struct prefixmsg
+            + "140001002A0079E10ABCF6050000000000000000";  // PREFIX_ADDRESS
+
+    @Test
+    public void testParseRtmNewPrefix_withoutPrefixCacheinfoAttribute() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWPREFIX_WITHOUT_PREFIX_CACHEINFO_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNull(msg);
+    }
+
+    private static final String RTM_NEWPREFIX_TRUNCATED_PREFIX_ADDRESS_HEX =
+            "3C000000340000000000000000000000"            // struct nlmsghr
+            + "0A0000002F00000003400300"                  // struct prefixmsg
+            + "140001002A0079E10ABCF605000000000000"      // PREFIX_ADDRESS (truncated)
+            + "0C000200803A0900008D2700";                 // PREFIX_CACHEINFO
+
+    @Test
+    public void testParseRtmNewPrefix_truncatedPrefixAddressAttribute() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWPREFIX_TRUNCATED_PREFIX_ADDRESS_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNull(msg);
+    }
+
+    private static final String RTM_NEWPREFIX_TRUNCATED_PREFIX_CACHEINFO_HEX =
+            "3C000000340000000000000000000000"            // struct nlmsghr
+            + "0A0000002F00000003400300"                  // struct prefixmsg
+            + "140001002A0079E10ABCF6050000000000000000"  // PREFIX_ADDRESS
+            + "0C000200803A0900008D";                     // PREFIX_CACHEINFO (truncated)
+
+    @Test
+    public void testParseRtmNewPrefix_truncatedPrefixCacheinfoAttribute() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWPREFIX_TRUNCATED_PREFIX_CACHEINFO_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNull(msg);
+    }
+
+    @Test
+    public void testToString() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWPREFIX_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkPrefixMessage);
+        final RtNetlinkPrefixMessage prefixmsg = (RtNetlinkPrefixMessage) msg;
+        final String expected = "RtNetlinkPrefixMessage{ "
+                + "nlmsghdr{StructNlMsgHdr{ nlmsg_len{60}, nlmsg_type{52(RTM_NEWPREFIX)}, "
+                + "nlmsg_flags{0()}, nlmsg_seq{0}, nlmsg_pid{0} }}, "
+                + "prefixmsg{prefix_family: 10, prefix_ifindex: 47, prefix_type: 3, "
+                + "prefix_len: 64, prefix_flags: 3}, "
+                + "IP Prefix{2001:db8:1:1::/64}, "
+                + "preferred lifetime{604800}, valid lifetime{2592000} }";
+        assertEquals(expected, prefixmsg.toString());
+    }
+}
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
index 86aa8f1..ec486fb 100644
--- a/staticlibs/testutils/Android.bp
+++ b/staticlibs/testutils/Android.bp
@@ -93,6 +93,7 @@
     libs: ["tradefed"],
     test_suites: [
         "ats",
+        "automotive-general-tests",
         "device-tests",
         "general-tests",
         "cts",
diff --git a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
index 8e27c62..c42d9e5 100644
--- a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
+++ b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
@@ -19,7 +19,7 @@
 import android.Manifest.permission.NETWORK_SETTINGS
 import android.content.pm.PackageManager.FEATURE_TELEPHONY
 import android.content.pm.PackageManager.FEATURE_WIFI
-import android.net.LinkAddress
+import android.net.InetAddresses.parseNumericAddress
 import android.net.Network
 import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
 import android.net.NetworkCapabilities.TRANSPORT_WIFI
@@ -66,7 +66,8 @@
     // Skip IPv6 checks on virtual devices which do not support it. Tests that require IPv6 will
     // still fail even if the preparer does not.
     private fun ipv6Unsupported(wifiSsid: String?) = ConnectUtil.VIRTUAL_SSIDS.contains(
-        WifiInfo.sanitizeSsid(wifiSsid))
+        WifiInfo.sanitizeSsid(wifiSsid)
+    )
 
     @Test
     fun testCheckWifiSetup() {
@@ -89,13 +90,25 @@
                 pos = 0,
                 timeoutMs = 30_000L
             ) {
-                it is LinkPropertiesChanged &&
-                it.network == network &&
-                it.lp.allLinkAddresses.any(LinkAddress::isIpv4) &&
-                        (ipv6Unsupported(ssid) || it.lp.hasGlobalIpv6Address())
+                if (it !is LinkPropertiesChanged || it.network != network) {
+                    false
+                } else {
+                    // Same check as used by DnsResolver for AI_ADDRCONFIG (have_ipv4)
+                    val ipv4Reachable = it.lp.isReachable(parseNumericAddress("8.8.8.8"))
+                    // Same check as used by DnsResolver for AI_ADDRCONFIG (have_ipv6)
+                    val ipv6Reachable = it.lp.isReachable(parseNumericAddress("2000::"))
+                    ipv4Reachable && (ipv6Unsupported(ssid) || ipv6Reachable)
+                }
             }
-            assertNotNull(lpChange, "Wifi network $network needs an IPv4 address" +
-                    if (ipv6Unsupported(ssid)) "" else " and a global IPv6 address")
+            assertNotNull(
+                lpChange,
+                "Wifi network $network needs an IPv4 address and default route" +
+                        if (ipv6Unsupported(ssid)) {
+                            ""
+                        } else {
+                            " and a global IPv6 address and default route"
+                        }
+            )
 
             Pair(network, ssid)
         }
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/CarrierConfigRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/CarrierConfigRule.kt
new file mode 100644
index 0000000..ae0de79
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/CarrierConfigRule.kt
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils.com.android.testutils
+
+import android.Manifest.permission.MODIFY_PHONE_STATE
+import android.Manifest.permission.READ_PHONE_STATE
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.ConditionVariable
+import android.os.PersistableBundle
+import android.telephony.CarrierConfigManager
+import android.telephony.CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED
+import android.telephony.SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX
+import android.util.Log
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.modules.utils.build.SdkLevel.isAtLeastU
+import com.android.testutils.runAsShell
+import com.android.testutils.tryTest
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+private val TAG = CarrierConfigRule::class.simpleName
+private const val CARRIER_CONFIG_CHANGE_TIMEOUT_MS = 10_000L
+
+/**
+ * A [TestRule] that helps set [CarrierConfigManager] overrides for tests and clean up the test
+ * configuration automatically on teardown.
+ */
+class CarrierConfigRule : TestRule {
+    private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
+    private val ccm by lazy { context.getSystemService(CarrierConfigManager::class.java) }
+
+    // Map of (subId) -> (original values of overridden settings)
+    private val originalConfigs = mutableMapOf<Int, PersistableBundle>()
+
+    override fun apply(base: Statement, description: Description): Statement {
+        return CarrierConfigStatement(base, description)
+    }
+
+    private inner class CarrierConfigStatement(
+        private val base: Statement,
+        private val description: Description
+    ) : Statement() {
+        override fun evaluate() {
+            tryTest {
+                base.evaluate()
+            } cleanup {
+                cleanUpNow()
+            }
+        }
+    }
+
+    private class ConfigChangeReceiver(private val subId: Int) : BroadcastReceiver() {
+        val cv = ConditionVariable()
+        override fun onReceive(context: Context, intent: Intent) {
+            if (intent.action != ACTION_CARRIER_CONFIG_CHANGED ||
+                intent.getIntExtra(EXTRA_SUBSCRIPTION_INDEX, -1) != subId) {
+                return
+            }
+            // This may race with other config changes for the same subId, but there is no way to
+            // know which update is being reported, and querying the override would return the
+            // latest values even before the config is applied. Config changes should be rare, so it
+            // is unlikely they would happen exactly after the override applied here and cause
+            // flakes.
+            cv.open()
+        }
+    }
+
+    private fun overrideConfigAndWait(subId: Int, config: PersistableBundle) {
+        val changeReceiver = ConfigChangeReceiver(subId)
+        context.registerReceiver(changeReceiver, IntentFilter(ACTION_CARRIER_CONFIG_CHANGED))
+        ccm.overrideConfig(subId, config)
+        assertTrue(
+            changeReceiver.cv.block(CARRIER_CONFIG_CHANGE_TIMEOUT_MS),
+            "Timed out waiting for config change for subId $subId"
+        )
+        context.unregisterReceiver(changeReceiver)
+    }
+
+    /**
+     * Add carrier config overrides with the specified configuration.
+     *
+     * The overrides will automatically be cleaned up when the test case finishes.
+     */
+    fun addConfigOverrides(subId: Int, config: PersistableBundle) {
+        val originalConfig = originalConfigs.computeIfAbsent(subId) { PersistableBundle() }
+        val overrideKeys = config.keySet()
+        val previousValues = runAsShell(READ_PHONE_STATE) {
+            ccm.getConfigForSubIdCompat(subId, overrideKeys)
+        }
+        // If a key is already in the originalConfig, keep the oldest original overrides
+        originalConfig.keySet().forEach {
+            previousValues.remove(it)
+        }
+        originalConfig.putAll(previousValues)
+
+        runAsShell(MODIFY_PHONE_STATE) {
+            overrideConfigAndWait(subId, config)
+        }
+    }
+
+    /**
+     * Cleanup overrides that were added by the test case.
+     *
+     * This will be called automatically on test teardown, so it does not need to be called by the
+     * test case unless cleaning up earlier is required.
+     */
+    fun cleanUpNow() {
+        runAsShell(MODIFY_PHONE_STATE) {
+            originalConfigs.forEach { (subId, config) ->
+                try {
+                    // Do not use null as the config to reset, as it would reset configs that may
+                    // have been set by target preparers such as
+                    // ConnectivityTestTargetPreparer / CarrierConfigSetupTest.
+                    overrideConfigAndWait(subId, config)
+                } catch (e: Throwable) {
+                    Log.e(TAG, "Error resetting carrier config for subId $subId")
+                }
+            }
+            originalConfigs.clear()
+        }
+    }
+}
+
+private fun CarrierConfigManager.getConfigForSubIdCompat(
+    subId: Int,
+    keys: Set<String>
+): PersistableBundle {
+    return if (isAtLeastU()) {
+        // This method is U+
+        getConfigForSubId(subId, *keys.toTypedArray())
+    } else {
+        @Suppress("DEPRECATION")
+        val config = assertNotNull(getConfigForSubId(subId))
+        val allKeys = config.keySet().toList()
+        allKeys.forEach {
+            if (!keys.contains(it)) {
+                config.remove(it)
+            }
+        }
+        config
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
index e5b8471..4b9429b 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
@@ -43,9 +43,14 @@
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.modules.utils.build.SdkLevel.isAtLeastS
 import java.io.ByteArrayOutputStream
+import java.io.CharArrayWriter
 import java.io.File
 import java.io.FileOutputStream
+import java.io.FileReader
+import java.io.OutputStream
+import java.io.OutputStreamWriter
 import java.io.PrintWriter
+import java.io.Reader
 import java.time.ZonedDateTime
 import java.time.format.DateTimeFormatter
 import java.util.concurrent.CompletableFuture
@@ -80,7 +85,38 @@
         var instance: ConnectivityDiagnosticsCollector? = null
     }
 
+    /**
+     * Indicates tcpdump should be started and written to the diagnostics file on test case failure.
+     */
+    annotation class CollectTcpdumpOnFailure
+
+    private class DumpThread(
+        // Keep a reference to the ParcelFileDescriptor otherwise GC would close it
+        private val fd: ParcelFileDescriptor,
+        private val reader: Reader
+    ) : Thread() {
+        private val writer = CharArrayWriter()
+        override fun run() {
+            reader.copyTo(writer)
+        }
+
+        fun closeAndWriteTo(output: OutputStream?) {
+            join()
+            fd.close()
+            if (output != null) {
+                val outputWriter = OutputStreamWriter(output)
+                outputWriter.write("--- tcpdump stopped at ${ZonedDateTime.now()} ---\n")
+                writer.writeTo(outputWriter)
+            }
+        }
+    }
+
+    private data class TcpdumpRun(val pid: Int, val reader: DumpThread)
+
     private var failureHeader: String? = null
+
+    // Accessed from the test listener methods which are synchronized by junit (see TestListener)
+    private var tcpdumpRun: TcpdumpRun? = null
     private val buffer = ByteArrayOutputStream()
     private val failureHeaderExtras = mutableMapOf<String, Any>()
     private val collectorDir: File by lazy {
@@ -132,7 +168,8 @@
                     .addCapability(NET_CAPABILITY_INTERNET)
                     .addTransportType(TRANSPORT_WIFI)
                     .addTransportType(TRANSPORT_CELLULAR)
-                    .build(), networkCallback
+                    .build(),
+                networkCallback
             )
         }
     }
@@ -148,16 +185,69 @@
         // when iterating on failing tests.
         if (!runOnFailure(failure.exception)) return
         if (outputFiles.size >= MAX_DUMPS) return
-        Log.i(TAG, "Collecting diagnostics for test failure. Disable by running tests with: " +
+        Log.i(
+            TAG,
+            "Collecting diagnostics for test failure. Disable by running tests with: " +
                 "atest MyModule -- " +
-                "--module-arg MyModule:instrumentation-arg:$ARG_RUN_ON_FAILURE:=false")
+                "--module-arg MyModule:instrumentation-arg:$ARG_RUN_ON_FAILURE:=false"
+        )
         collectTestFailureDiagnostics(failure.exception)
 
         val baseFilename = "${description.className}#${description.methodName}_failure"
         flushBufferToFileMetric(testData, baseFilename)
     }
 
+    override fun onTestStart(testData: DataRecord, description: Description) {
+        val tcpdumpAnn = description.annotations.firstOrNull { it is CollectTcpdumpOnFailure }
+                as? CollectTcpdumpOnFailure
+        if (tcpdumpAnn != null) {
+            startTcpdumpForTestcaseIfSupported()
+        }
+    }
+
+    private fun startTcpdumpForTestcaseIfSupported() {
+        if (!DeviceInfoUtils.isDebuggable()) {
+            Log.d(TAG, "Cannot start tcpdump, build is not debuggable")
+            return
+        }
+        if (tcpdumpRun != null) {
+            Log.e(TAG, "Cannot start tcpdump: it is already running")
+            return
+        }
+        // executeShellCommand won't tokenize quoted arguments containing spaces (like pcap filters)
+        // properly, so pass in the command in stdin instead of using sh -c 'command'
+        val fds = instrumentation.uiAutomation.executeShellCommandRw("sh")
+
+        val stdout = fds[0]
+        val stdin = fds[1]
+        ParcelFileDescriptor.AutoCloseOutputStream(stdin).use { writer ->
+            // Echo the current pid, and replace it (with exec) with the tcpdump process, so the
+            // tcpdump pid is known.
+            writer.write(
+                "echo $$; exec su 0 tcpdump -n -i any -U -xx".encodeToByteArray()
+            )
+        }
+        val reader = FileReader(stdout.fileDescriptor).buffered()
+        val tcpdumpPid = Integer.parseInt(reader.readLine())
+        val dumpThread = DumpThread(stdout, reader)
+        dumpThread.start()
+        tcpdumpRun = TcpdumpRun(tcpdumpPid, dumpThread)
+    }
+
+    private fun stopTcpdumpIfRunning(output: OutputStream?) {
+        val run = tcpdumpRun ?: return
+        // Send SIGTERM for graceful shutdown of tcpdump so that it can flush its output
+        executeCommandBlocking("su 0 kill ${run.pid}")
+        run.reader.closeAndWriteTo(output)
+        tcpdumpRun = null
+    }
+
     override fun onTestEnd(testData: DataRecord, description: Description) {
+        // onTestFail is called before onTestEnd, so if the test failed tcpdump would already have
+        // been stopped and output dumped. Here this stops tcpdump if the test succeeded, throwing
+        // away its output.
+        stopTcpdumpIfRunning(output = null)
+
         // Tests may call methods like collectDumpsysConnectivity to collect diagnostics at any time
         // during the run, for example to observe state at various points to investigate a flake
         // and compare passing/failing cases.
@@ -196,6 +286,7 @@
                 fos.write("\n".toByteArray())
             }
             fos.write(buffer.toByteArray())
+            stopTcpdumpIfRunning(fos)
         }
         failureHeader = null
         buffer.reset()
@@ -239,8 +330,11 @@
                 }
             }
         } else {
-            Log.w(TAG, "The test is still holding shell permissions, cannot collect privileged " +
-                    "device info")
+            Log.w(
+                TAG,
+                "The test is still holding shell permissions, cannot collect privileged " +
+                    "device info"
+            )
             headerObj.put("shellPermissionsUnavailable", true)
         }
         failureHeader = headerObj.apply {
@@ -292,7 +386,9 @@
         cbHelper.registerNetworkCallback(
             NetworkRequest.Builder()
                 .addTransportType(TRANSPORT_WIFI)
-                .addCapability(NET_CAPABILITY_INTERNET).build(), cb)
+                .addCapability(NET_CAPABILITY_INTERNET).build(),
+            cb
+        )
         return try {
             cb.wifiInfoFuture.get(1L, TimeUnit.SECONDS)
         } catch (e: TimeoutException) {
@@ -323,16 +419,43 @@
      * @param exceptionContext An exception to write a stacktrace to the dump for context.
      */
     fun collectDumpsysConnectivity(exceptionContext: Throwable? = null) {
-        Log.i(TAG, "Collecting dumpsys connectivity for test artifacts")
+        collectDumpsys("connectivity --dump-priority HIGH", exceptionContext)
+    }
+
+    /**
+     * Add a dumpsys to the test data dump.
+     *
+     * <p>The dump will be collected immediately, and exported to a test artifact file when the
+     * test ends.
+     * @param dumpsysCmd The dumpsys command to run (for example "connectivity").
+     * @param exceptionContext An exception to write a stacktrace to the dump for context.
+     */
+    fun collectDumpsys(dumpsysCmd: String, exceptionContext: Throwable? = null) =
+        collectCommandOutput("dumpsys $dumpsysCmd", exceptionContext = exceptionContext)
+
+    /**
+     * Add the output of a command to the test data dump.
+     *
+     * <p>The output will be collected immediately, and exported to a test artifact file when the
+     * test ends.
+     * @param cmd The command to run. Stdout of the command will be collected.
+     * @param shell The shell to run the command in.
+     * @param exceptionContext An exception to write a stacktrace to the dump for context.
+     */
+    fun collectCommandOutput(
+        cmd: String,
+        shell: String = "sh",
+        exceptionContext: Throwable? = null
+    ) {
+        Log.i(TAG, "Collecting '$cmd' for test artifacts")
         PrintWriter(buffer).let {
-            it.println("--- Dumpsys connectivity at ${ZonedDateTime.now()} ---")
+            it.println("--- $cmd at ${ZonedDateTime.now()} ---")
             maybeWriteExceptionContext(it, exceptionContext)
             it.flush()
         }
-        ParcelFileDescriptor.AutoCloseInputStream(
-            InstrumentationRegistry.getInstrumentation().uiAutomation.executeShellCommand(
-                "dumpsys connectivity --dump-priority HIGH")).use {
-            it.copyTo(buffer)
+
+        runCommandInShell(cmd, shell) { stdout, _ ->
+            stdout.copyTo(buffer)
         }
     }
 
@@ -350,4 +473,4 @@
         writer.println("At: ")
         exceptionContext.printStackTrace(writer)
     }
-}
\ No newline at end of file
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PollingUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/PollingUtils.kt
new file mode 100644
index 0000000..0a0290a
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PollingUtils.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+private const val POLLING_INTERVAL_MS: Int = 100
+
+/** Calls condition() until it returns true or timeout occurs. */
+fun pollingCheck(condition: () -> Boolean, timeout_ms: Int): Boolean {
+    var polling_time = 0
+    do {
+        Thread.sleep(POLLING_INTERVAL_MS.toLong())
+        polling_time += POLLING_INTERVAL_MS
+        if (condition()) return true
+    } while (polling_time < timeout_ms)
+    return false
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/SetFeatureFlagsRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/SetFeatureFlagsRule.kt
index 7b970d3..0b239b4 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/SetFeatureFlagsRule.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/SetFeatureFlagsRule.kt
@@ -68,10 +68,15 @@
      * If any `@FeatureFlag` annotation is found, it passes every feature flag's name
      * and enabled state into the user-specified lambda to apply custom actions.
      */
+    private val parameterizedRegexp = Regex("\\[\\d+\\]$")
     override fun apply(base: Statement, description: Description): Statement {
         return object : Statement() {
             override fun evaluate() {
-                val testMethod = description.testClass.getMethod(description.methodName)
+                // If the same class also uses Parameterized, depending on evaluation order the
+                // method names here may be synthetic method names, where [0] [1] or so are added
+                // at the end of the method name. Find the original method name.
+                val methodName = description.methodName.replace(parameterizedRegexp, "")
+                val testMethod = description.testClass.getMethod(methodName)
                 val featureFlagAnnotations = testMethod.getAnnotationsByType(
                     FeatureFlag::class.java
                 )
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ShellUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/ShellUtil.kt
new file mode 100644
index 0000000..fadc2ab
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ShellUtil.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("ShellUtil")
+
+package com.android.testutils
+
+import android.app.UiAutomation
+import android.os.ParcelFileDescriptor.AutoCloseInputStream
+import android.os.ParcelFileDescriptor.AutoCloseOutputStream
+import androidx.test.platform.app.InstrumentationRegistry
+import java.io.InputStream
+
+/**
+ * Run a command in a shell.
+ *
+ * Compared to [UiAutomation.executeShellCommand], this allows running commands with pipes and
+ * redirections. [UiAutomation.executeShellCommand] splits the command on spaces regardless of
+ * quotes, so it is not able to run commands like `sh -c "echo 123 > some_file"`.
+ *
+ * @param cmd Shell command to run.
+ * @param shell Command used to run the shell.
+ * @param outputProcessor Function taking stdout, stderr as argument. The streams will be closed
+ *                        when this function returns.
+ * @return Result of [outputProcessor].
+ */
+fun <T> runCommandInShell(
+    cmd: String,
+    shell: String = "sh",
+    outputProcessor: (InputStream, InputStream) -> T,
+): T {
+    val (stdout, stdin, stderr) = InstrumentationRegistry.getInstrumentation().uiAutomation
+        .executeShellCommandRwe(shell)
+    AutoCloseOutputStream(stdin).bufferedWriter().use { it.write(cmd) }
+    AutoCloseInputStream(stdout).use { outStream ->
+        AutoCloseInputStream(stderr).use { errStream ->
+            return outputProcessor(outStream, errStream)
+        }
+    }
+}
+
+/**
+ * Run a command in a shell.
+ *
+ * Overload of [runCommandInShell] that reads and returns stdout as String.
+ */
+fun runCommandInShell(
+    cmd: String,
+    shell: String = "sh",
+) = runCommandInShell(cmd, shell) { stdout, _ ->
+    stdout.reader().use { it.readText() }
+}
+
+/**
+ * Run a command in a root shell.
+ *
+ * This is generally only usable on devices on which [DeviceInfoUtils.isDebuggable] is true.
+ * @see runCommandInShell
+ */
+fun runCommandInRootShell(
+    cmd: String
+) = runCommandInShell(cmd, shell = "su root sh")
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
index 8dc1bc4..bfbbc34 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
@@ -14,19 +14,34 @@
  * limitations under the License.
  */
 
-package com.android.testutils;
+package com.android.testutils
 
 import android.content.Context
+import android.net.InetAddresses.parseNumericAddress
 import android.net.KeepalivePacketData
+import android.net.LinkAddress
 import android.net.LinkProperties
 import android.net.NetworkAgent
 import android.net.NetworkAgentConfig
 import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.TRANSPORT_TEST
 import android.net.NetworkProvider
+import android.net.NetworkRequest
 import android.net.QosFilter
 import android.net.Uri
 import android.os.Looper
+import android.system.ErrnoException
+import android.system.Os
+import android.system.OsConstants
+import android.system.OsConstants.EADDRNOTAVAIL
+import android.system.OsConstants.ENETUNREACH
+import android.system.OsConstants.ENONET
+import android.system.OsConstants.IPPROTO_UDP
+import android.system.OsConstants.SOCK_DGRAM
+import com.android.modules.utils.build.SdkLevel.isAtLeastS
 import com.android.net.module.util.ArrayTrackRecord
+import com.android.testutils.CompatUtil.makeTestNetworkSpecifier
 import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnAddKeepalivePacketFilter
 import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnAutomaticReconnectDisabled
 import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnBandwidthUpdateRequested
@@ -42,6 +57,8 @@
 import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnStopSocketKeepalive
 import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnUnregisterQosCallback
 import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnValidationStatus
+import java.net.NetworkInterface
+import java.net.SocketException
 import java.time.Duration
 import kotlin.test.assertEquals
 import kotlin.test.assertNotNull
@@ -65,6 +82,92 @@
     conf: NetworkAgentConfig
 ) : NetworkAgent(context, looper, TestableNetworkAgent::class.java.simpleName /* tag */,
         nc, lp, TEST_NETWORK_SCORE, conf, Provider(context, looper)) {
+    companion object {
+
+        /**
+         * Convenience method to create a [NetworkRequest] matching [TestableNetworkAgent]s from
+         * [createOnInterface].
+         */
+        fun makeNetworkRequestForInterface(ifaceName: String) = NetworkRequest.Builder()
+            .removeCapability(NET_CAPABILITY_TRUSTED)
+            .addTransportType(TRANSPORT_TEST)
+            .setNetworkSpecifier(makeTestNetworkSpecifier(ifaceName))
+            .build()
+
+        /**
+         * Convenience method to initialize a [TestableNetworkAgent] on a given interface.
+         *
+         * This waits for link-local addresses to be setup and ensures LinkProperties are updated
+         * with the addresses.
+         */
+        fun createOnInterface(
+            context: Context,
+            looper: Looper,
+            ifaceName: String,
+            timeoutMs: Long
+        ): TestableNetworkAgent {
+            val lp = LinkProperties().apply {
+                interfaceName = ifaceName
+            }
+            val agent = TestableNetworkAgent(
+                context,
+                looper,
+                NetworkCapabilities().apply {
+                    removeCapability(NET_CAPABILITY_TRUSTED)
+                    addTransportType(TRANSPORT_TEST)
+                    setNetworkSpecifier(makeTestNetworkSpecifier(ifaceName))
+                },
+                lp,
+                NetworkAgentConfig.Builder().build()
+            )
+            val network = agent.register()
+            agent.markConnected()
+            if (isAtLeastS()) {
+                // OnNetworkCreated was added in S
+                agent.eventuallyExpect<OnNetworkCreated>()
+            }
+
+            // Wait until the link-local address can be used. Address flags are not available
+            // without elevated permissions, so check that bindSocket works.
+            assertEventuallyTrue("No usable v6 address after $timeoutMs ms", timeoutMs) {
+                // To avoid race condition between socket connection succeeding and interface
+                // returning a non-empty address list. Verify that interface returns a non-empty
+                // list, before trying the socket connection.
+                if (NetworkInterface.getByName(ifaceName).interfaceAddresses.isEmpty()) {
+                    return@assertEventuallyTrue false
+                }
+
+                val sock = Os.socket(OsConstants.AF_INET6, SOCK_DGRAM, IPPROTO_UDP)
+                tryTest {
+                    network.bindSocket(sock)
+                    Os.connect(sock, parseNumericAddress("ff02::fb%$ifaceName"), 12345)
+                    true
+                }.catch<ErrnoException> {
+                    if (it.errno != ENETUNREACH && it.errno != EADDRNOTAVAIL) {
+                        throw it
+                    }
+                    false
+                }.catch<SocketException> {
+                    // OnNetworkCreated does not exist on R, so a SocketException caused by ENONET
+                    // may be seen before the network is created
+                    if (isAtLeastS()) throw it
+                    val cause = it.cause as? ErrnoException ?: throw it
+                    if (cause.errno != ENONET) {
+                        throw it
+                    }
+                    false
+                } cleanup {
+                    Os.close(sock)
+                }
+            }
+
+            agent.lp.setLinkAddresses(NetworkInterface.getByName(ifaceName).interfaceAddresses.map {
+                LinkAddress(it.address, it.networkPrefixLength.toInt())
+            })
+            agent.sendLinkProperties(agent.lp)
+            return agent
+        }
+    }
 
     val DEFAULT_TIMEOUT_MS = 5000L
 
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
index ae43c15..d9c51e5 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
@@ -32,6 +32,7 @@
 import com.android.testutils.RecorderCallback.CallbackEntry.LocalInfoChanged
 import com.android.testutils.RecorderCallback.CallbackEntry.Losing
 import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.RecorderCallback.CallbackEntry.Reserved
 import com.android.testutils.RecorderCallback.CallbackEntry.Resumed
 import com.android.testutils.RecorderCallback.CallbackEntry.Suspended
 import com.android.testutils.RecorderCallback.CallbackEntry.Unavailable
@@ -66,6 +67,12 @@
         // constructor by specifying override.
         abstract val network: Network
 
+        data class Reserved private constructor(
+                override val network: Network,
+                val caps: NetworkCapabilities
+        ): CallbackEntry() {
+            constructor(caps: NetworkCapabilities) : this(NULL_NETWORK, caps)
+        }
         data class Available(override val network: Network) : CallbackEntry()
         data class CapabilitiesChanged(
             override val network: Network,
@@ -100,6 +107,8 @@
         // Convenience constants for expecting a type
         companion object {
             @JvmField
+            val RESERVED = Reserved::class
+            @JvmField
             val AVAILABLE = Available::class
             @JvmField
             val NETWORK_CAPS_UPDATED = CapabilitiesChanged::class
@@ -127,6 +136,11 @@
     val history = backingRecord.newReadHead()
     val mark get() = history.mark
 
+    override fun onReserved(caps: NetworkCapabilities) {
+        Log.d(logTag, "onReserved $caps")
+        history.add(Reserved(caps))
+    }
+
     override fun onAvailable(network: Network) {
         Log.d(logTag, "onAvailable $network")
         history.add(Available(network))
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkOfferCallback.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkOfferCallback.kt
index 21bd60c..a0078d2 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkOfferCallback.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkOfferCallback.kt
@@ -52,10 +52,11 @@
 
     inline fun <reified T : CallbackEntry> expectCallbackThat(
         crossinline predicate: (T) -> Boolean
-    ) {
+    ): T {
         val event = history.poll(timeoutMs)
                 ?: fail("Did not receive callback after ${timeoutMs}ms")
         if (event !is T || !predicate(event)) fail("Received unexpected callback $event")
+        return event
     }
 
     fun expectOnNetworkNeeded(capabilities: NetworkCapabilities) =
diff --git a/staticlibs/testutils/host/java/com/android/testutils/ConnectivityTestTargetPreparer.kt b/staticlibs/testutils/host/java/com/android/testutils/ConnectivityTestTargetPreparer.kt
index f6168af..a99359b 100644
--- a/staticlibs/testutils/host/java/com/android/testutils/ConnectivityTestTargetPreparer.kt
+++ b/staticlibs/testutils/host/java/com/android/testutils/ConnectivityTestTargetPreparer.kt
@@ -18,12 +18,15 @@
 
 import com.android.ddmlib.testrunner.TestResult
 import com.android.tradefed.config.Option
+import com.android.tradefed.invoker.ExecutionFiles.FilesKey
 import com.android.tradefed.invoker.TestInformation
+import com.android.tradefed.log.LogUtil.CLog
 import com.android.tradefed.result.CollectingTestListener
 import com.android.tradefed.result.ddmlib.DefaultRemoteAndroidTestRunner
 import com.android.tradefed.targetprep.BaseTargetPreparer
 import com.android.tradefed.targetprep.TargetSetupError
 import com.android.tradefed.targetprep.suite.SuiteApkInstaller
+import java.io.File
 
 private const val CONNECTIVITY_CHECKER_APK = "ConnectivityTestPreparer.apk"
 private const val CONNECTIVITY_PKG_NAME = "com.android.testutils.connectivitypreparer"
@@ -48,7 +51,41 @@
  * --test-arg com.android.testutils.ConnectivityTestTargetPreparer:ignore-wifi-check:true".
  */
 open class ConnectivityTestTargetPreparer : BaseTargetPreparer() {
-    private val installer = SuiteApkInstaller()
+    private val installer = ApkInstaller()
+
+    private class ApkInstaller : SuiteApkInstaller() {
+        override fun getLocalPathForFilename(
+            testInfo: TestInformation,
+            apkFileName: String
+        ): File {
+            if (apkFileName == CONNECTIVITY_CHECKER_APK) {
+                // For the connectivity checker APK, explicitly look for it in the directory of the
+                // host-side preparer.
+                // This preparer is part of the net-tests-utils-host-common library, which includes
+                // the checker APK via device_common_data in its build rule. Both need to be at the
+                // same version so that the preparer calls the right test methods in the checker
+                // APK.
+                // The default strategy for finding test files is to do a recursive search in test
+                // directories, which may find wrong files in wrong directories. In particular,
+                // if some MTS test includes the checker APK, and that test is linked to a module
+                // that boards the train at a version different from this target preparer, there
+                // could be a version difference between the APK and the host-side preparer.
+                // Explicitly looking for the APK in the host-side preparer directory ensures that
+                // it uses the version that was packaged together with the host-side preparer.
+                val testsDir = testInfo.executionFiles().get(FilesKey.TESTS_DIRECTORY)
+                val f = File(testsDir, "net-tests-utils-host-common/$CONNECTIVITY_CHECKER_APK")
+                if (f.isFile) {
+                    return f
+                }
+                // When running locally via atest, device_common_data does cause the APK to be put
+                // into the test temp directory, so recursive search is still used to find it in the
+                // directory of the test module that is being run. This is fine because atest runs
+                // are on local trees that do not have versioning problems.
+                CLog.i("APK not found at $f, falling back to recursive search")
+            }
+            return super.getLocalPathForFilename(testInfo, apkFileName)
+        }
+    }
 
     @Option(
         name = IGNORE_WIFI_CHECK,
@@ -179,7 +216,7 @@
                 .contains(":deny")
         }
 
-    private fun refreshTime(testInfo: TestInformation,) {
+    private fun refreshTime(testInfo: TestInformation) {
         // Forces a synchronous time refresh using the network. Time is fetched synchronously but
         // this does not guarantee that system time is updated when it returns.
         // This avoids flakes where the system clock rolls back, for example when using test
diff --git a/staticlibs/testutils/host/python/tether_utils.py b/staticlibs/testutils/host/python/tether_utils.py
index 702b596..710f8a8 100644
--- a/staticlibs/testutils/host/python/tether_utils.py
+++ b/staticlibs/testutils/host/python/tether_utils.py
@@ -95,7 +95,9 @@
   hotspot_interface = server.startHotspot(test_ssid, test_passphrase)
 
   # Make the client connects to the hotspot.
-  client_network = client.connectToWifi(test_ssid, test_passphrase)
+  client_network = client.connectToWifi(
+      test_ssid, test_passphrase, upstream_type != UpstreamType.NONE
+  )
 
   return hotspot_interface, client_network
 
@@ -108,3 +110,6 @@
     server.unregisterAll()
   # Teardown the hotspot.
   server.stopAllTethering()
+  # Some test cases would disable wifi, e.g. cellular upstream tests.
+  # Reconnect to it if feasible.
+  server.reconnectWifiIfSupported()
diff --git a/tests/common/java/android/net/CaptivePortalTest.java b/tests/common/java/android/net/CaptivePortalTest.java
index 15d3398..6655827 100644
--- a/tests/common/java/android/net/CaptivePortalTest.java
+++ b/tests/common/java/android/net/CaptivePortalTest.java
@@ -19,6 +19,7 @@
 import static org.junit.Assert.assertEquals;
 
 import android.os.Build;
+import android.os.IBinder;
 import android.os.RemoteException;
 
 import androidx.test.filters.SmallTest;
@@ -55,6 +56,10 @@
             mCode = request;
         }
 
+        @Override
+        public void setDelegateUid(int uid, IBinder binder, IIntResultListener listener) {
+        }
+
         // This is only @Override on R-
         public void logEvent(int eventId, String packageName) throws RemoteException {
             mCode = eventId;
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index 0f0e2f1..d694637 100644
--- a/tests/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -382,6 +382,7 @@
             netCap.setAllowedUids(allowedUids);
             netCap.setSubscriptionIds(Set.of(TEST_SUBID1, TEST_SUBID2));
             netCap.setUids(uids);
+            netCap.setReservationId(42);
         }
 
         netCap.setOwnerUid(123);
@@ -1493,4 +1494,42 @@
         // nc1 and nc2 are the same since invalid capability is ignored
         assertEquals(nc1, nc2);
     }
+
+    @Test
+    public void testReservationIdMatching() {
+        final NetworkCapabilities requestNc = new NetworkCapabilities();
+        final NetworkCapabilities reservationNc = new NetworkCapabilities();
+        reservationNc.setReservationId(42);
+
+        final NetworkCapabilities reservedNetworkNc = new NetworkCapabilities(reservationNc);
+        final NetworkCapabilities otherNetworkNc = new NetworkCapabilities();
+        final NetworkCapabilities otherReservedNetworkNc = new NetworkCapabilities();
+        otherReservedNetworkNc.setReservationId(99);
+        final NetworkCapabilities offerNc = new NetworkCapabilities();
+        offerNc.setReservationId(NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS);
+
+        // A regular request can match any network or offer except one with MATCH_ALL_RESERVATIONS
+        assertTrue(requestNc.satisfiedByNetworkCapabilities(reservedNetworkNc));
+        assertTrue(requestNc.satisfiedByNetworkCapabilities(otherNetworkNc));
+        assertTrue(requestNc.satisfiedByNetworkCapabilities(otherReservedNetworkNc));
+        assertFalse(requestNc.satisfiedByNetworkCapabilities(offerNc));
+
+        // A reservation request can only match the reservedNetwork and the blanket offer.
+        assertTrue(reservationNc.satisfiedByNetworkCapabilities(reservedNetworkNc));
+        assertFalse(reservationNc.satisfiedByNetworkCapabilities(otherNetworkNc));
+        assertFalse(reservationNc.satisfiedByNetworkCapabilities(otherReservedNetworkNc));
+        assertTrue(reservationNc.satisfiedByNetworkCapabilities(offerNc));
+    }
+
+    @Test @IgnoreUpTo(SC_V2)
+    public void testReservationIdEquals() {
+        final NetworkCapabilities nc = new NetworkCapabilities();
+        nc.setReservationId(42);
+        final NetworkCapabilities other = new NetworkCapabilities(nc);
+
+        assertEquals(nc, other);
+
+        nc.setReservationId(43);
+        assertNotEquals(nc, other);
+    }
 }
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java
index ad98a29..a1cf968 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java
@@ -16,6 +16,8 @@
 
 package com.android.cts.net.hostside;
 
+import static android.net.TetheringManager.TETHERING_WIFI;
+
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
 import static org.junit.Assert.assertEquals;
@@ -25,6 +27,7 @@
 import android.content.Context;
 import android.net.TetheringInterface;
 import android.net.cts.util.CtsTetheringUtils;
+import android.net.cts.util.CtsTetheringUtils.TestTetheringEventCallback;
 import android.net.wifi.SoftApConfiguration;
 import android.net.wifi.WifiSsid;
 
@@ -37,6 +40,7 @@
 public class TetheringTest {
     private CtsTetheringUtils mCtsTetheringUtils;
     private TetheringHelperClient mTetheringHelperClient;
+    private TestTetheringEventCallback mTetheringEventCallback;
 
     @Before
     public void setUp() throws Exception {
@@ -44,11 +48,14 @@
         mCtsTetheringUtils = new CtsTetheringUtils(targetContext);
         mTetheringHelperClient = new TetheringHelperClient(targetContext);
         mTetheringHelperClient.bind();
+        mTetheringEventCallback = mCtsTetheringUtils.registerTetheringEventCallback();
     }
 
     @After
     public void tearDown() throws Exception {
         mTetheringHelperClient.unbind();
+        mCtsTetheringUtils.unregisterTetheringEventCallback(mTetheringEventCallback);
+        mCtsTetheringUtils.stopAllTethering();
     }
 
     /**
@@ -57,24 +64,23 @@
      */
     @Test
     public void testSoftApConfigurationRedactedForOtherUids() throws Exception {
-        final CtsTetheringUtils.TestTetheringEventCallback tetherEventCallback =
-                mCtsTetheringUtils.registerTetheringEventCallback();
+        mTetheringEventCallback.assumeWifiTetheringSupported(
+                getInstrumentation().getTargetContext());
         SoftApConfiguration softApConfig = new SoftApConfiguration.Builder()
                 .setWifiSsid(WifiSsid.fromBytes("This is an SSID!"
                         .getBytes(StandardCharsets.UTF_8))).build();
         final TetheringInterface tetheringInterface =
-                mCtsTetheringUtils.startWifiTethering(tetherEventCallback, softApConfig);
+                mCtsTetheringUtils.startWifiTethering(mTetheringEventCallback, softApConfig);
         assertNotNull(tetheringInterface);
         assertEquals(softApConfig, tetheringInterface.getSoftApConfiguration());
-        try {
-            TetheringInterface tetheringInterfaceForApp2 =
-                    mTetheringHelperClient.getTetheredWifiInterface();
-            assertNotNull(tetheringInterfaceForApp2);
-            assertNull(tetheringInterfaceForApp2.getSoftApConfiguration());
-            assertEquals(
-                    tetheringInterface.getInterface(), tetheringInterfaceForApp2.getInterface());
-        } finally {
-            mCtsTetheringUtils.stopWifiTethering(tetherEventCallback);
-        }
+        assertEquals(new TetheringInterface(
+                TETHERING_WIFI, tetheringInterface.getInterface(), softApConfig),
+                tetheringInterface);
+        TetheringInterface tetheringInterfaceForApp2 =
+                mTetheringHelperClient.getTetheredWifiInterface();
+        assertNotNull(tetheringInterfaceForApp2);
+        assertNull(tetheringInterfaceForApp2.getSoftApConfiguration());
+        assertEquals(
+                tetheringInterface.getInterface(), tetheringInterfaceForApp2.getInterface());
     }
 }
diff --git a/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java b/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
index ae572e6..b5e2450 100644
--- a/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
+++ b/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
@@ -91,8 +91,8 @@
     }
 
     private String[] getSysctlDirs() throws Exception {
-        String interfaceDirs[] = mDevice.executeAdbCommand("shell", "ls", "-1",
-                IPV6_SYSCTL_DIR).split("\n");
+        String[] interfaceDirs = mDevice.executeShellCommand("ls -1 " + IPV6_SYSCTL_DIR)
+                .split("\n");
         List<String> interfaceDirsList = new ArrayList<String>(Arrays.asList(interfaceDirs));
         interfaceDirsList.remove("all");
         interfaceDirsList.remove("lo");
@@ -109,13 +109,13 @@
     }
 
     public int readIntFromPath(String path) throws Exception {
-        String mode = mDevice.executeAdbCommand("shell", "stat", "-c", "%a", path).trim();
-        String user = mDevice.executeAdbCommand("shell", "stat", "-c", "%u", path).trim();
-        String group = mDevice.executeAdbCommand("shell", "stat", "-c", "%g", path).trim();
+        String mode = mDevice.executeShellCommand("stat -c %a " + path).trim();
+        String user = mDevice.executeShellCommand("stat -c %u " + path).trim();
+        String group = mDevice.executeShellCommand("stat -c %g " + path).trim();
         assertEquals(mode, "644");
         assertEquals(user, "0");
         assertEquals(group, "0");
-        return Integer.parseInt(mDevice.executeAdbCommand("shell", "cat", path).trim());
+        return Integer.parseInt(mDevice.executeShellCommand("cat " + path).trim());
     }
 
     /**
@@ -191,7 +191,7 @@
         assumeTrue(new DeviceSdkLevel(mDevice).isDeviceAtLeastV());
 
         String path = "/proc/sys/net/ipv4/tcp_congestion_control";
-        String value = mDevice.executeAdbCommand("shell", "cat", path).trim();
+        String value = mDevice.executeShellCommand("cat " + path).trim();
         assertEquals("cubic", value);
     }
 }
diff --git a/tests/cts/multidevices/Android.bp b/tests/cts/multidevices/Android.bp
index 949be85..a082a95 100644
--- a/tests/cts/multidevices/Android.bp
+++ b/tests/cts/multidevices/Android.bp
@@ -22,6 +22,7 @@
     main: "run_tests.py",
     srcs: [
         "apfv4_test.py",
+        "apfv6_test.py",
         "connectivity_multi_devices_test.py",
         "run_tests.py",
     ],
diff --git a/tests/cts/multidevices/apfv4_test.py b/tests/cts/multidevices/apfv4_test.py
index 7795be5..aa535fd 100644
--- a/tests/cts/multidevices/apfv4_test.py
+++ b/tests/cts/multidevices/apfv4_test.py
@@ -53,7 +53,7 @@
   )  # Declare inputs for state_str and expected_result.
   def test_apf_drop_ethertype_not_allowed(self, blocked_ether_type):
     # Ethernet header (14 bytes).
-    packet = ETHER_BROADCAST_ADDR  # Destination MAC (broadcast)
+    packet = self.client_mac_address.replace(":", "")  # Destination MAC
     packet += self.server_mac_address.replace(":", "")  # Source MAC
     packet += blocked_ether_type
 
diff --git a/tests/cts/multidevices/apfv6_test.py b/tests/cts/multidevices/apfv6_test.py
new file mode 100644
index 0000000..fc732d2
--- /dev/null
+++ b/tests/cts/multidevices/apfv6_test.py
@@ -0,0 +1,84 @@
+#  Copyright (C) 2024 The Android Open Source Project
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+from mobly import asserts
+from net_tests_utils.host.python import apf_test_base, apf_utils, adb_utils, assert_utils, packet_utils
+
+APFV6_VERSION = 6000
+ARP_OFFLOAD_REPLY_LEN = 60
+
+class ApfV6Test(apf_test_base.ApfTestBase):
+    def setup_class(self):
+        super().setup_class()
+
+        # Skip tests for APF version < 6000
+        apf_utils.assume_apf_version_support_at_least(
+            self.clientDevice, self.client_iface_name, APFV6_VERSION
+        )
+
+    def teardown_class(self):
+        # force to stop capture on the server device if any test case failed
+        try:
+            apf_utils.stop_capture_packets(self.serverDevice, self.server_iface_name)
+        except assert_utils.UnexpectedBehaviorError:
+            pass
+        super().teardown_class()
+
+    def test_unicast_arp_request_offload(self):
+        arp_request = packet_utils.construct_arp_packet(
+            src_mac=self.server_mac_address,
+            dst_mac=self.client_mac_address,
+            src_ip=self.server_ipv4_addresses[0],
+            dst_ip=self.client_ipv4_addresses[0],
+            op=packet_utils.ARP_REQUEST_OP
+        )
+
+        arp_reply = packet_utils.construct_arp_packet(
+            src_mac=self.client_mac_address,
+            dst_mac=self.server_mac_address,
+            src_ip=self.client_ipv4_addresses[0],
+            dst_ip=self.server_ipv4_addresses[0],
+            op=packet_utils.ARP_REPLY_OP
+        )
+
+        # Add zero padding up to 60 bytes, since APFv6 ARP offload always sent out 60 bytes reply
+        arp_reply = arp_reply.ljust(ARP_OFFLOAD_REPLY_LEN * 2, "0")
+
+        self.send_packet_and_expect_reply_received(
+            arp_request, "DROPPED_ARP_REQUEST_REPLIED", arp_reply
+        )
+
+    def test_broadcast_arp_request_offload(self):
+        arp_request = packet_utils.construct_arp_packet(
+            src_mac=self.server_mac_address,
+            dst_mac=packet_utils.ETHER_BROADCAST_MAC_ADDRESS,
+            src_ip=self.server_ipv4_addresses[0],
+            dst_ip=self.client_ipv4_addresses[0],
+            op=packet_utils.ARP_REQUEST_OP
+        )
+
+        arp_reply = packet_utils.construct_arp_packet(
+            src_mac=self.client_mac_address,
+            dst_mac=self.server_mac_address,
+            src_ip=self.client_ipv4_addresses[0],
+            dst_ip=self.server_ipv4_addresses[0],
+            op=packet_utils.ARP_REPLY_OP
+        )
+
+        # Add zero padding up to 60 bytes, since APFv6 ARP offload always sent out 60 bytes reply
+        arp_reply = arp_reply.ljust(ARP_OFFLOAD_REPLY_LEN * 2, "0")
+
+        self.send_packet_and_expect_reply_received(
+            arp_request, "DROPPED_ARP_REQUEST_REPLIED", arp_reply
+        )
diff --git a/tests/cts/multidevices/run_tests.py b/tests/cts/multidevices/run_tests.py
index 1391d13..a0d0bec 100644
--- a/tests/cts/multidevices/run_tests.py
+++ b/tests/cts/multidevices/run_tests.py
@@ -16,6 +16,7 @@
 
 import sys
 from apfv4_test import ApfV4Test
+from apfv6_test import ApfV6Test
 from connectivity_multi_devices_test import ConnectivityMultiDevicesTest
 from mobly import suite_runner
 
@@ -35,4 +36,4 @@
     index = sys.argv.index("--")
     sys.argv = sys.argv[:1] + sys.argv[index + 1 :]
   # TODO: make the tests can be executed without manually list classes.
-  suite_runner.run_suite([ConnectivityMultiDevicesTest, ApfV4Test], sys.argv)
+  suite_runner.run_suite([ConnectivityMultiDevicesTest, ApfV4Test, ApfV6Test], sys.argv)
diff --git a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
index 49688cc..252052e 100644
--- a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
+++ b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
@@ -27,13 +27,11 @@
 import android.net.NetworkRequest
 import android.net.cts.util.CtsNetUtils
 import android.net.cts.util.CtsTetheringUtils
-import android.net.wifi.ScanResult
 import android.net.wifi.SoftApConfiguration
 import android.net.wifi.SoftApConfiguration.SECURITY_TYPE_WPA2_PSK
 import android.net.wifi.WifiConfiguration
 import android.net.wifi.WifiInfo
 import android.net.wifi.WifiManager
-import android.net.wifi.WifiNetworkSpecifier
 import android.net.wifi.WifiSsid
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.compatibility.common.util.PropertyUtil
@@ -89,6 +87,11 @@
         ctsNetUtils.expectNetworkIsSystemDefault(network)
     }
 
+    @Rpc(description = "Reconnect to wifi if supported.")
+    fun reconnectWifiIfSupported() {
+        ctsNetUtils.reconnectWifiIfSupported()
+    }
+
     @Rpc(description = "Unregister all connections.")
     fun unregisterAll() {
         cbHelper.unregisterAll()
@@ -104,10 +107,7 @@
     // Suppress warning because WifiManager methods to connect to a config are
     // documented not to be deprecated for privileged users.
     @Suppress("DEPRECATION")
-    fun connectToWifi(ssid: String, passphrase: String): Long {
-        val specifier = WifiNetworkSpecifier.Builder()
-            .setBand(ScanResult.WIFI_BAND_24_GHZ)
-            .build()
+    fun connectToWifi(ssid: String, passphrase: String, requireValidation: Boolean): Long {
         val wifiConfig = WifiConfiguration()
         wifiConfig.SSID = "\"" + ssid + "\""
         wifiConfig.preSharedKey = "\"" + passphrase + "\""
@@ -136,7 +136,8 @@
             return@runAsShell networkCallback.eventuallyExpect<CapabilitiesChanged> {
                 // Remove double quotes.
                 val ssidFromCaps = (WifiInfo::sanitizeSsid)(it.caps.ssid)
-                ssidFromCaps == ssid && it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
+                ssidFromCaps == ssid && (!requireValidation ||
+                        it.caps.hasCapability(NET_CAPABILITY_VALIDATED))
             }.network.networkHandle
         }
     }
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index a9ac29c..1ba581a 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -95,6 +95,7 @@
         "NetworkStackApiCurrentShims",
     ],
     test_suites: [
+        "automotive-general-tests",
         "cts",
         "mts-tethering",
         "mcts-tethering",
@@ -160,6 +161,7 @@
     min_sdk_version: "30",
     // Tag this module as a cts test artifact
     test_suites: [
+        "automotive-general-tests",
         "cts",
         "general-tests",
     ],
diff --git a/tests/cts/net/AndroidTestTemplate.xml b/tests/cts/net/AndroidTestTemplate.xml
index 55b6494..cb0e575 100644
--- a/tests/cts/net/AndroidTestTemplate.xml
+++ b/tests/cts/net/AndroidTestTemplate.xml
@@ -42,6 +42,7 @@
     </target_preparer>
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="{PACKAGE}" />
+        <option name="shell-timeout" value="1500s"/>
         <option name="runtime-hint" value="9m4s" />
         <option name="hidden-api-checks" value="false" />
         <option name="isolated-storage" value="false" />
diff --git a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
index 320622b..dee5f71 100644
--- a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
+++ b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
@@ -19,10 +19,8 @@
 
 package android.net.cts
 
-import android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG
-import android.Manifest.permission.WRITE_DEVICE_CONFIG
-import android.content.pm.PackageManager
 import android.content.pm.PackageManager.FEATURE_AUTOMOTIVE
+import android.content.pm.PackageManager.FEATURE_LEANBACK
 import android.content.pm.PackageManager.FEATURE_WIFI
 import android.net.ConnectivityManager
 import android.net.Network
@@ -38,7 +36,7 @@
 import android.net.apf.ApfConstants.IPV6_NEXT_HEADER_OFFSET
 import android.net.apf.ApfConstants.IPV6_SRC_ADDR_OFFSET
 import android.net.apf.ApfCounterTracker
-import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_MULTICAST_PING
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_NS_REPLIED_NON_DAD
 import android.net.apf.ApfCounterTracker.Counter.FILTER_AGE_16384THS
 import android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_ICMP
 import android.net.apf.ApfV4Generator
@@ -52,14 +50,15 @@
 import android.os.Handler
 import android.os.HandlerThread
 import android.os.PowerManager
+import android.os.SystemProperties
 import android.os.UserManager
 import android.platform.test.annotations.AppModeFull
-import android.provider.DeviceConfig
-import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
 import android.system.Os
 import android.system.OsConstants
 import android.system.OsConstants.AF_INET6
 import android.system.OsConstants.ETH_P_IPV6
+import android.system.OsConstants.ICMP6_ECHO_REPLY
+import android.system.OsConstants.ICMP6_ECHO_REQUEST
 import android.system.OsConstants.IPPROTO_ICMPV6
 import android.system.OsConstants.SOCK_DGRAM
 import android.system.OsConstants.SOCK_NONBLOCK
@@ -87,7 +86,7 @@
 import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
 import com.android.testutils.SkipPresubmit
 import com.android.testutils.TestableNetworkCallback
-import com.android.testutils.runAsShell
+import com.android.testutils.pollingCheck
 import com.android.testutils.waitForIdle
 import com.google.common.truth.Expect
 import com.google.common.truth.Truth.assertThat
@@ -104,6 +103,7 @@
 import kotlin.test.assertNotNull
 import org.junit.After
 import org.junit.AfterClass
+import org.junit.Assume.assumeFalse
 import org.junit.Before
 import org.junit.BeforeClass
 import org.junit.Rule
@@ -112,8 +112,6 @@
 
 private const val TAG = "ApfIntegrationTest"
 private const val TIMEOUT_MS = 2000L
-private const val APF_NEW_RA_FILTER_VERSION = "apf_new_ra_filter_version"
-private const val POLLING_INTERVAL_MS: Int = 100
 private const val RCV_BUFFER_SIZE = 1480
 private const val PING_HEADER_LENGTH = 8
 
@@ -131,16 +129,6 @@
         private val powerManager = context.getSystemService(PowerManager::class.java)!!
         private val wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG)
 
-        fun pollingCheck(condition: () -> Boolean, timeout_ms: Int): Boolean {
-            var polling_time = 0
-            do {
-                Thread.sleep(POLLING_INTERVAL_MS.toLong())
-                polling_time += POLLING_INTERVAL_MS
-                if (condition()) return true
-            } while (polling_time < timeout_ms)
-            return false
-        }
-
         fun turnScreenOff() {
             if (!wakeLock.isHeld()) wakeLock.acquire()
             runShellCommandOrThrow("input keyevent KEYCODE_SLEEP")
@@ -170,8 +158,8 @@
         private fun isAutomotiveWithVisibleBackgroundUser(): Boolean {
             val packageManager = context.getPackageManager()
             val userManager = context.getSystemService(UserManager::class.java)!!
-            return (packageManager.hasSystemFeature(FEATURE_AUTOMOTIVE)
-                    && userManager.isVisibleBackgroundUsersSupported)
+            return (packageManager.hasSystemFeature(FEATURE_AUTOMOTIVE) &&
+                    userManager.isVisibleBackgroundUsersSupported)
         }
 
         @BeforeClass
@@ -188,16 +176,6 @@
             Thread.sleep(1000)
             // TODO: check that there is no active wifi network. Otherwise, ApfFilter has already been
             // created.
-            // APF adb cmds are only implemented in ApfFilter.java. Enable experiment to prevent
-            // LegacyApfFilter.java from being used.
-            runAsShell(WRITE_DEVICE_CONFIG, WRITE_ALLOWLISTED_DEVICE_CONFIG) {
-                DeviceConfig.setProperty(
-                        NAMESPACE_CONNECTIVITY,
-                        APF_NEW_RA_FILTER_VERSION,
-                        "1",  // value => force enabled
-                        false // makeDefault
-                )
-            }
         }
 
         @AfterClass
@@ -211,8 +189,13 @@
             handler: Handler,
             private val network: Network
     ) : PacketReader(handler, RCV_BUFFER_SIZE) {
+        private data class PingContext(
+            val futureReply: CompletableFuture<List<ByteArray>>,
+            val expectReplyCount: Int,
+            val replyPayloads: MutableList<ByteArray> = mutableListOf()
+        )
         private var sockFd: FileDescriptor? = null
-        private var futureReply: CompletableFuture<ByteArray>? = null
+        private var pingContext: PingContext? = null
 
         override fun createFd(): FileDescriptor {
             // sockFd is closed by calling super.stop()
@@ -224,6 +207,8 @@
         }
 
         override fun handlePacket(recvbuf: ByteArray, length: Int) {
+            val context = pingContext ?: return
+
             // If zero-length or Type is not echo reply: ignore.
             if (length == 0 || recvbuf[0] != 0x81.toByte()) {
                 return
@@ -231,10 +216,14 @@
             // Only copy the ping data and complete the future.
             val result = recvbuf.sliceArray(8..<length)
             Log.i(TAG, "Received ping reply: ${result.toHexString()}")
-            futureReply!!.complete(recvbuf.sliceArray(8..<length))
+            context.replyPayloads.add(recvbuf.sliceArray(8..<length))
+            if (context.replyPayloads.size == context.expectReplyCount) {
+                context.futureReply.complete(context.replyPayloads)
+                pingContext = null
+            }
         }
 
-        fun sendPing(data: ByteArray, payloadSize: Int) {
+        fun sendPing(data: ByteArray, payloadSize: Int, expectReplyCount: Int = 1) {
             require(data.size == payloadSize)
 
             // rfc4443#section-4.1: Echo Request Message
@@ -250,17 +239,20 @@
             val icmp6Header = byteArrayOf(0x80.toByte(), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
             val packet = icmp6Header + data
             Log.i(TAG, "Sent ping: ${packet.toHexString()}")
-            futureReply = CompletableFuture<ByteArray>()
+            pingContext = PingContext(
+                futureReply = CompletableFuture<List<ByteArray>>(),
+                expectReplyCount = expectReplyCount
+            )
             Os.sendto(sockFd!!, packet, 0, packet.size, 0, PING_DESTINATION)
         }
 
-        fun expectPingReply(timeoutMs: Long = TIMEOUT_MS): ByteArray {
-            return futureReply!!.get(timeoutMs, TimeUnit.MILLISECONDS)
+        fun expectPingReply(timeoutMs: Long = TIMEOUT_MS): List<ByteArray> {
+            return pingContext!!.futureReply.get(timeoutMs, TimeUnit.MILLISECONDS)
         }
 
         fun expectPingDropped() {
             assertFailsWith(TimeoutException::class) {
-                futureReply!!.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+                pingContext!!.futureReply.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
             }
         }
 
@@ -299,10 +291,23 @@
         return ApfCapabilities(version, maxLen, packetFormat)
     }
 
+    private fun isTvDeviceSupportFullNetworkingUnder2w(): Boolean {
+        return (pm.hasSystemFeature(FEATURE_LEANBACK) &&
+            pm.hasSystemFeature("com.google.android.tv.full_networking_under_2w"))
+    }
+
     @Before
     fun setUp() {
         assume().that(pm.hasSystemFeature(FEATURE_WIFI)).isTrue()
 
+        // Based on GTVS-16, Android Packet Filtering (APF) is OPTIONAL for devices that fully
+        // process all network packets on CPU at all times, even in standby, while meeting
+        // the <= 2W standby power demand requirement.
+        assumeFalse(
+            "Skipping test: TV device process full networking on CPU under 2W",
+            isTvDeviceSupportFullNetworkingUnder2w()
+        )
+
         networkCallback = TestableNetworkCallback()
         cm.requestNetwork(
                 NetworkRequest.Builder()
@@ -349,10 +354,8 @@
     @Test
     fun testApfCapabilities() {
         // APF became mandatory in Android 14 VSR.
-        assume().that(getVsrApiLevel()).isAtLeast(34)
-
-        // ApfFilter does not support anything but ARPHRD_ETHER.
-        assertThat(caps.apfPacketFormat).isEqualTo(OsConstants.ARPHRD_ETHER)
+        val vsrApiLevel = getVsrApiLevel()
+        assume().that(vsrApiLevel).isAtLeast(34)
 
         // DEVICEs launching with Android 14 with CHIPSETs that set ro.board.first_api_level to 34:
         // - [GMS-VSR-5.3.12-003] MUST return 4 or higher as the APF version number from calls to
@@ -372,9 +375,22 @@
         // ro.board.first_api_level or ro.board.api_level to 202404 or higher:
         // - [GMS-VSR-5.3.12-009] MUST indicate at least 2048 bytes of usable memory from calls to
         //   the getApfPacketFilterCapabilities HAL method.
-        if (getVsrApiLevel() >= 202404) {
+        if (vsrApiLevel >= 202404) {
             assertThat(caps.maximumApfProgramSize).isAtLeast(2048)
         }
+
+        // CHIPSETs (or DEVICES with CHIPSETs) that set ro.board.first_api_level or
+        // ro.board.api_level to 202504 or higher:
+        // - [VSR-5.3.12-018] MUST implement version 6 of the Android Packet Filtering (APF)
+        //   interpreter in the Wi-Fi firmware.
+        // - [VSR-5.3.12-019] MUST provide at least 4000 bytes of APF RAM.
+        if (vsrApiLevel >= 202504) {
+            assertThat(caps.apfVersionSupported).isEqualTo(6000)
+            assertThat(caps.maximumApfProgramSize).isAtLeast(4000)
+        }
+
+        // ApfFilter does not support anything but ARPHRD_ETHER.
+        assertThat(caps.apfPacketFormat).isEqualTo(OsConstants.ARPHRD_ETHER)
     }
 
     // APF is backwards compatible, i.e. a v6 interpreter supports both v2 and v4 functionality.
@@ -382,6 +398,10 @@
         assume().that(caps.apfVersionSupported).isAtLeast(version)
     }
 
+    fun assumeNotCuttlefish() {
+        assume().that(SystemProperties.get("ro.product.board", "")).isNotEqualTo("cutf")
+    }
+
     fun installProgram(bytes: ByteArray) {
         val prog = bytes.toHexString()
         val result = runShellCommandOrThrow("cmd network_stack apf $ifname install $prog").trim()
@@ -443,15 +463,15 @@
 
     fun ApfV4GeneratorBase<*>.addPassIfNotIcmpv6EchoReply() {
         // If not IPv6 -> PASS
-        addLoad16(R0, ETH_ETHERTYPE_OFFSET)
+        addLoad16intoR0(ETH_ETHERTYPE_OFFSET)
         addJumpIfR0NotEquals(ETH_P_IPV6.toLong(), BaseApfGenerator.PASS_LABEL)
 
         // If not ICMPv6 -> PASS
-        addLoad8(R0, IPV6_NEXT_HEADER_OFFSET)
+        addLoad8intoR0(IPV6_NEXT_HEADER_OFFSET)
         addJumpIfR0NotEquals(IPPROTO_ICMPV6.toLong(), BaseApfGenerator.PASS_LABEL)
 
         // If not echo reply -> PASS
-        addLoad8(R0, ICMP6_TYPE_OFFSET)
+        addLoad8intoR0(ICMP6_TYPE_OFFSET)
         addJumpIfR0NotEquals(0x81, BaseApfGenerator.PASS_LABEL)
     }
 
@@ -465,6 +485,7 @@
         // should be turned on.
         assume().that(getVsrApiLevel()).isAtLeast(34)
         assumeApfVersionSupportAtLeast(4)
+        assumeNotCuttlefish()
 
         // clear any active APF filter
         clearApfMemory()
@@ -478,7 +499,7 @@
         }
         val data = ByteArray(payloadSize).also { Random.nextBytes(it) }
         packetReader.sendPing(data, payloadSize)
-        assertThat(packetReader.expectPingReply()).isEqualTo(data)
+        assertThat(packetReader.expectPingReply()[0]).isEqualTo(data)
 
         // Generate an APF program that drops the next ping
         val gen = ApfV4Generator(
@@ -517,6 +538,7 @@
         assume().that(getVsrApiLevel()).isAtLeast(34)
         // Test v4 memory slots on both v4 and v6 interpreters.
         assumeApfVersionSupportAtLeast(4)
+        assumeNotCuttlefish()
         clearApfMemory()
         val gen = ApfV4Generator(
                 caps.apfVersionSupported,
@@ -543,6 +565,13 @@
 
         val program = gen.generate()
         assertThat(program.size).isLessThan(counterRegion)
+        val randomProgram = ByteArray(1) { 0 } +
+                ByteArray(counterRegion - 1).also { Random.nextBytes(it) }
+        // There are known firmware bugs where they calculate the number of non-zero bytes within
+        // the program to determine the program length. Modify the test to first install a longer
+        // program before installing a program that do the program length check. This should help us
+        // catch these types of firmware bugs in CTS. (b/395545572)
+        installAndVerifyProgram(randomProgram)
         installAndVerifyProgram(program)
 
         // Trigger the program by sending a ping and waiting on the reply.
@@ -575,6 +604,7 @@
         // should be turned on.
         assume().that(getVsrApiLevel()).isAtLeast(34)
         assumeApfVersionSupportAtLeast(4)
+        assumeNotCuttlefish()
         clearApfMemory()
         val gen = ApfV4Generator(
                 caps.apfVersionSupported,
@@ -617,6 +647,7 @@
     @Test
     fun testFilterAge16384thsIncreasesBetweenPackets() {
         assumeApfVersionSupportAtLeast(6000)
+        assumeNotCuttlefish()
         clearApfMemory()
         val gen = ApfV6Generator(
                 caps.apfVersionSupported,
@@ -666,6 +697,7 @@
     @Test
     fun testReplyPing() {
         assumeApfVersionSupportAtLeast(6000)
+        assumeNotCuttlefish()
         installProgram(ByteArray(caps.maximumApfProgramSize) { 0 }) // Clear previous program
         readProgram() // Ensure installation is complete
 
@@ -690,69 +722,80 @@
         //     increase PASSED_IPV6_ICMP counter
         //     pass
         //   else
-        //     transmit a ICMPv6 echo request packet with the first byte of the payload in the reply
-        //     increase DROPPED_IPV6_MULTICAST_PING counter
+        //     transmit 3 ICMPv6 echo requests with random first byte
+        //     increase DROPPED_IPV6_NS_REPLIED_NON_DAD counter
         //     drop
-        val program = gen
-                .addLoad16(R0, ETH_ETHERTYPE_OFFSET)
+        gen.addLoad16intoR0(ETH_ETHERTYPE_OFFSET)
                 .addJumpIfR0NotEquals(ETH_P_IPV6.toLong(), skipPacketLabel)
-                .addLoad8(R0, IPV6_NEXT_HEADER_OFFSET)
+                .addLoad8intoR0(IPV6_NEXT_HEADER_OFFSET)
                 .addJumpIfR0NotEquals(IPPROTO_ICMPV6.toLong(), skipPacketLabel)
-                .addLoad8(R0, ICMP6_TYPE_OFFSET)
-                .addJumpIfR0NotEquals(0x81, skipPacketLabel) // Echo reply type
+                .addLoad8intoR0(ICMP6_TYPE_OFFSET)
+                .addJumpIfR0NotEquals(ICMP6_ECHO_REPLY.toLong(), skipPacketLabel)
                 .addLoadFromMemory(R0, MemorySlot.PACKET_SIZE)
                 .addCountAndPassIfR0Equals(
-                        (ETHER_HEADER_LEN + IPV6_HEADER_LEN + PING_HEADER_LENGTH + firstByte.size)
-                                .toLong(),
-                        PASSED_IPV6_ICMP
+                    (ETHER_HEADER_LEN + IPV6_HEADER_LEN + PING_HEADER_LENGTH + firstByte.size)
+                        .toLong(),
+                    PASSED_IPV6_ICMP
                 )
-                // Ping Packet Generation
-                .addAllocate(pingRequestPktLen)
-                // Eth header
-                .addPacketCopy(ETHER_SRC_ADDR_OFFSET, ETHER_ADDR_LEN) // dst MAC address
-                .addPacketCopy(ETHER_DST_ADDR_OFFSET, ETHER_ADDR_LEN) // src MAC address
-                .addWriteU16(ETH_P_IPV6) // IPv6 type
-                // IPv6 Header
-                .addWrite32(0x60000000) // IPv6 Header: version, traffic class, flowlabel
-                // payload length (2 bytes) | next header: ICMPv6 (1 byte) | hop limit (1 byte)
-                .addWrite32(pingRequestIpv6PayloadLen shl 16 or (IPPROTO_ICMPV6 shl 8 or 64))
-                .addPacketCopy(IPV6_DEST_ADDR_OFFSET, IPV6_ADDR_LEN) // src ip
-                .addPacketCopy(IPV6_SRC_ADDR_OFFSET, IPV6_ADDR_LEN) // dst ip
-                // ICMPv6
-                .addWriteU8(0x80) // type: echo request
-                .addWriteU8(0) // code
-                .addWriteU16(pingRequestIpv6PayloadLen) // checksum
-                // identifier
-                .addPacketCopy(ETHER_HEADER_LEN + IPV6_HEADER_LEN + ICMPV6_HEADER_MIN_LEN, 2)
-                .addWriteU16(0) // sequence number
-                .addDataCopy(firstByte) // data
-                .addTransmitL4(
+
+        val numOfPacketToTransmit = 3
+        val expectReplyPayloads = (0 until numOfPacketToTransmit).map { Random.nextBytes(1) }
+        expectReplyPayloads.forEach { replyPingPayload ->
+            // Ping Packet Generation
+            gen.addAllocate(pingRequestPktLen)
+                    // Eth header
+                    .addPacketCopy(ETHER_SRC_ADDR_OFFSET, ETHER_ADDR_LEN) // dst MAC address
+                    .addPacketCopy(ETHER_DST_ADDR_OFFSET, ETHER_ADDR_LEN) // src MAC address
+                    .addWriteU16(ETH_P_IPV6) // IPv6 type
+                    // IPv6 Header
+                    .addWrite32(0x60000000) // IPv6 Header: version, traffic class, flowlabel
+                    // payload length (2 bytes) | next header: ICMPv6 (1 byte) | hop limit (1 byte)
+                    .addWrite32(pingRequestIpv6PayloadLen shl 16 or (IPPROTO_ICMPV6 shl 8 or 64))
+                    .addPacketCopy(IPV6_DEST_ADDR_OFFSET, IPV6_ADDR_LEN) // src ip
+                    .addPacketCopy(IPV6_SRC_ADDR_OFFSET, IPV6_ADDR_LEN) // dst ip
+                    // ICMPv6
+                    .addWriteU8(ICMP6_ECHO_REQUEST)
+                    .addWriteU8(0) // code
+                    .addWriteU16(pingRequestIpv6PayloadLen) // checksum
+                    // identifier
+                    .addPacketCopy(ETHER_HEADER_LEN + IPV6_HEADER_LEN + ICMPV6_HEADER_MIN_LEN, 2)
+                    .addWriteU16(0) // sequence number
+                    .addDataCopy(replyPingPayload) // data
+                    .addTransmitL4(
                         ETHER_HEADER_LEN, // ip_ofs
                         ICMP6_CHECKSUM_OFFSET, // csum_ofs
                         IPV6_SRC_ADDR_OFFSET, // csum_start
                         IPPROTO_ICMPV6, // partial_sum
                         false // udp
-                )
-                // Warning: the program abuse DROPPED_IPV6_MULTICAST_PING for debugging purpose
-                .addCountAndDrop(DROPPED_IPV6_MULTICAST_PING)
-                .defineLabel(skipPacketLabel)
-                .addPass()
-                .generate()
+                    )
+        }
 
+        // Warning: the program abuse DROPPED_IPV6_NS_REPLIED_NON_DAD for debugging purpose
+        gen.addCountAndDrop(DROPPED_IPV6_NS_REPLIED_NON_DAD)
+            .defineLabel(skipPacketLabel)
+            .addPass()
+
+        val program = gen.generate()
         installAndVerifyProgram(program)
 
-        packetReader.sendPing(payload, payloadSize)
-
-        val replyPayload = try {
+        packetReader.sendPing(payload, payloadSize, expectReplyCount = numOfPacketToTransmit)
+        val replyPayloads = try {
             packetReader.expectPingReply(TIMEOUT_MS * 2)
         } catch (e: TimeoutException) {
-            byteArrayOf() // Empty payload if timeout occurs
+            emptyList()
         }
 
         val apfCounterTracker = ApfCounterTracker()
         apfCounterTracker.updateCountersFromData(readProgram())
         Log.i(TAG, "counter map: ${apfCounterTracker.counters}")
 
-        assertThat(replyPayload).isEqualTo(firstByte)
+        assertThat(replyPayloads.size).isEqualTo(expectReplyPayloads.size)
+
+        // Sort the payload list before comparison to ensure consistency.
+        val sortedReplyPayloads = replyPayloads.sortedBy { it[0] }
+        val sortedExpectReplyPayloads = expectReplyPayloads.sortedBy { it[0] }
+        for (i in sortedReplyPayloads.indices) {
+            assertThat(sortedReplyPayloads[i]).isEqualTo(sortedExpectReplyPayloads[i])
+        }
     }
 }
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
index ceb48d4..faaadee 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
@@ -88,9 +88,11 @@
 import com.android.net.module.util.ArrayTrackRecord;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.com.android.testutils.CarrierConfigRule;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -110,6 +112,9 @@
 public class ConnectivityDiagnosticsManagerTest {
     private static final String TAG = ConnectivityDiagnosticsManagerTest.class.getSimpleName();
 
+    @Rule
+    public final CarrierConfigRule mCarrierConfigRule = new CarrierConfigRule();
+
     private static final int CALLBACK_TIMEOUT_MILLIS = 5000;
     private static final int NO_CALLBACK_INVOKED_TIMEOUT = 500;
     private static final long TIMESTAMP = 123456789L;
@@ -264,9 +269,6 @@
             doBroadcastCarrierConfigsAndVerifyOnConnectivityReportAvailable(
                     subId, carrierConfigReceiver, testNetworkCallback);
         }, () -> {
-                runWithShellPermissionIdentity(
-                    () -> mCarrierConfigManager.overrideConfig(subId, null),
-                    android.Manifest.permission.MODIFY_PHONE_STATE);
             mConnectivityManager.unregisterNetworkCallback(testNetworkCallback);
             mContext.unregisterReceiver(carrierConfigReceiver);
             });
@@ -291,9 +293,9 @@
                 CarrierConfigManager.KEY_CARRIER_CERTIFICATE_STRING_ARRAY,
                 new String[] {getCertHashForThisPackage()});
 
+        mCarrierConfigRule.addConfigOverrides(subId, carrierConfigs);
         runWithShellPermissionIdentity(
                 () -> {
-                    mCarrierConfigManager.overrideConfig(subId, carrierConfigs);
                     mCarrierConfigManager.notifyConfigChangedForSubId(subId);
                 },
                 android.Manifest.permission.MODIFY_PHONE_STATE);
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index feb4621..aa7d618 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -127,6 +127,7 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
 
 import android.annotation.NonNull;
@@ -922,6 +923,7 @@
     public void testOpenConnection() throws Exception {
         assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
         assumeTrue(mPackageManager.hasSystemFeature(FEATURE_TELEPHONY));
+        assumeFalse(Build.MODEL.contains("Cuttlefish"));
 
         Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
         Network cellNetwork = networkCallbackRule.requestCell();
@@ -1615,7 +1617,7 @@
         assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
         final ContentResolver resolver = mContext.getContentResolver();
         mCtsNetUtils.ensureWifiConnected();
-        final String ssid = unquoteSSID(mWifiManager.getConnectionInfo().getSSID());
+        final String ssid = unquoteSSID(getSSID());
         final String oldMeteredSetting = getWifiMeteredStatus(ssid);
         final String oldMeteredMultipathPreference = Settings.Global.getString(
                 resolver, NETWORK_METERED_MULTIPATH_PREFERENCE);
@@ -1628,7 +1630,7 @@
             // since R.
             final Network network = setWifiMeteredStatusAndWait(ssid, true /* isMetered */,
                     false /* waitForValidation */);
-            assertEquals(ssid, unquoteSSID(mWifiManager.getConnectionInfo().getSSID()));
+            assertEquals(ssid, unquoteSSID(getSSID()));
             assertEquals(mCm.getNetworkCapabilities(network).hasCapability(
                     NET_CAPABILITY_NOT_METERED), false);
             assertMultipathPreferenceIsEventually(network, initialMeteredPreference,
@@ -2329,8 +2331,10 @@
 
             // Verify that turning airplane mode off takes effect as expected.
             // connectToCell only registers a request, it cannot / does not need to be called twice
-            mCtsNetUtils.ensureWifiConnected();
-            if (verifyWifi) waitForAvailable(wifiCb);
+            if (verifyWifi) {
+                mCtsNetUtils.ensureWifiConnected();
+                waitForAvailable(wifiCb);
+            }
             if (supportTelephony) {
                 telephonyCb.eventuallyExpect(
                         CallbackEntry.AVAILABLE, CELL_DATA_AVAILABLE_TIMEOUT_MS);
@@ -2429,7 +2433,7 @@
                 mPackageManager.hasSystemFeature(FEATURE_WIFI));
 
         final Network network = mCtsNetUtils.ensureWifiConnected();
-        final String ssid = unquoteSSID(mWifiManager.getConnectionInfo().getSSID());
+        final String ssid = unquoteSSID(getSSID());
         assertNotNull("Ssid getting from WifiManager is null", ssid);
         // This package should have no NETWORK_SETTINGS permission. Verify that no ssid is contained
         // in the NetworkCapabilities.
@@ -2940,6 +2944,15 @@
                         new Handler(Looper.getMainLooper())), NETWORK_SETTINGS);
     }
 
+    /**
+     * It needs android.Manifest.permission.INTERACT_ACROSS_USERS_FULL
+     * to use WifiManager.getConnectionInfo() on the visible background user.
+     */
+    private String getSSID() {
+        return runWithShellPermissionIdentity(() ->
+                mWifiManager.getConnectionInfo().getSSID());
+    }
+
     private static final class OnCompleteListenerCallback {
         final CompletableFuture<Object> mDone = new CompletableFuture<>();
 
@@ -4074,4 +4087,11 @@
         // shims, and @IgnoreUpTo does not check that.
         assumeTrue(TestUtils.shouldTestSApis());
     }
+
+    @Test
+    public void testLegacyTetherApisThrowUnsupportedOperationExceptionAfterV() {
+        assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM);
+        assertThrows(UnsupportedOperationException.class, () -> mCm.tether("iface"));
+        assertThrows(UnsupportedOperationException.class, () -> mCm.untether("iface"));
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/DnsResolverTapTest.kt b/tests/cts/net/src/android/net/cts/DnsResolverTapTest.kt
new file mode 100644
index 0000000..ff608f2
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/DnsResolverTapTest.kt
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts
+
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.Manifest.permission.READ_DEVICE_CONFIG
+import android.net.DnsResolver
+import android.net.InetAddresses.parseNumericAddress
+import android.net.IpPrefix
+import android.net.MacAddress
+import android.net.RouteInfo
+import android.os.CancellationSignal
+import android.os.HandlerThread
+import android.os.SystemClock
+import android.platform.test.annotations.AppModeFull
+import android.provider.DeviceConfig
+import android.provider.DeviceConfig.NAMESPACE_NETD_NATIVE
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.UDP_HEADER_LEN
+import com.android.testutils.AutoReleaseNetworkCallbackRule
+import com.android.testutils.DeviceConfigRule
+import com.android.testutils.DnsResolverModuleTest
+import com.android.testutils.IPv6UdpFilter
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.RouterAdvertisementResponder
+import com.android.testutils.TapPacketReaderRule
+import com.android.testutils.TestableNetworkAgent
+import com.android.testutils.TestDnsPacket
+import com.android.testutils.com.android.testutils.SetFeatureFlagsRule
+import com.android.testutils.runAsShell
+import java.net.Inet6Address
+import java.net.InetAddress
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private val TEST_DNSSERVER_MAC = MacAddress.fromString("00:11:22:33:44:55")
+private val TAG = DnsResolverTapTest::class.java.simpleName
+private const val TEST_TIMEOUT_MS = 10_000L
+
+@AppModeFull(reason = "Test networks cannot be created in instant app mode")
+@DnsResolverModuleTest
+@RunWith(AndroidJUnit4::class)
+class DnsResolverTapTest {
+    private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
+    private val handlerThread = HandlerThread(TAG)
+
+    @get:Rule(order = 1)
+    val deviceConfigRule = DeviceConfigRule()
+
+    @get:Rule(order = 2)
+    val featureFlagsRule = SetFeatureFlagsRule(
+        setFlagsMethod = { name, enabled ->
+            val value = when (enabled) {
+                null -> null
+                true -> "1"
+                false -> "0"
+            }
+            deviceConfigRule.setConfig(NAMESPACE_NETD_NATIVE, name, value)
+        },
+        getFlagsMethod = {
+            runAsShell(READ_DEVICE_CONFIG) {
+                DeviceConfig.getInt(NAMESPACE_NETD_NATIVE, it, 0) == 1
+            }
+        }
+    )
+
+    @get:Rule(order = 3)
+    val packetReaderRule = TapPacketReaderRule()
+
+    @get:Rule(order = 4)
+    val cbRule = AutoReleaseNetworkCallbackRule()
+
+    private val ndResponder by lazy { RouterAdvertisementResponder(packetReaderRule.reader) }
+    private val dnsServerAddr by lazy {
+        parseNumericAddress("fe80::124%${packetReaderRule.iface.interfaceName}") as Inet6Address
+    }
+    private lateinit var agent: TestableNetworkAgent
+
+    @Before
+    fun setUp() {
+        handlerThread.start()
+        val interfaceName = packetReaderRule.iface.interfaceName
+        val cb = cbRule.requestNetwork(TestableNetworkAgent.makeNetworkRequestForInterface(
+            interfaceName))
+        agent = runAsShell(MANAGE_TEST_NETWORKS) {
+            TestableNetworkAgent.createOnInterface(context, handlerThread.looper,
+                interfaceName, TEST_TIMEOUT_MS)
+        }
+        ndResponder.addNeighborEntry(TEST_DNSSERVER_MAC, dnsServerAddr)
+        ndResponder.start()
+        agent.lp.apply {
+            addDnsServer(dnsServerAddr)
+            // A default route is needed for DnsResolver.java to send queries over IPv6
+            // (see usage of DnsUtils.haveIpv6).
+            addRoute(RouteInfo(IpPrefix("::/0"), null, null))
+        }
+        agent.sendLinkProperties(agent.lp)
+        cb.eventuallyExpect<LinkPropertiesChanged> { it.lp.dnsServers.isNotEmpty() }
+    }
+
+    @After
+    fun tearDown() {
+        ndResponder.stop()
+        if (::agent.isInitialized) {
+            agent.unregister()
+        }
+        handlerThread.quitSafely()
+        handlerThread.join()
+    }
+
+    private class DnsCallback : DnsResolver.Callback<List<InetAddress>> {
+        override fun onAnswer(answer: List<InetAddress>, rcode: Int) = Unit
+        override fun onError(error: DnsResolver.DnsException) = Unit
+    }
+
+    /**
+     * Run a cancellation test.
+     *
+     * @param domain Domain name to query
+     * @param waitTimeForNoRetryAfterCancellationMs If positive, cancel the query and wait for that
+     *                                              delay to check no retry is sent.
+     * @return The duration it took to receive all expected replies.
+     */
+    fun doCancellationTest(domain: String, waitTimeForNoRetryAfterCancellationMs: Long): Long {
+        val cancellationSignal = CancellationSignal()
+        val dnsCb = DnsCallback()
+        val queryStart = SystemClock.elapsedRealtime()
+        DnsResolver.getInstance().query(
+            agent.network, domain, 0 /* flags */,
+            Runnable::run /* executor */, cancellationSignal, dnsCb
+        )
+
+        if (waitTimeForNoRetryAfterCancellationMs > 0) {
+            cancellationSignal.cancel()
+        }
+        // Filter for queries on UDP port 53 for the specified domain
+        val filter = IPv6UdpFilter(dstPort = 53).and {
+            TestDnsPacket(
+                it.copyOfRange(ETHER_HEADER_LEN + IPV6_HEADER_LEN + UDP_HEADER_LEN, it.size),
+                dstAddr = dnsServerAddr
+            ).isQueryFor(domain, DnsResolver.TYPE_AAAA)
+        }
+
+        val reader = packetReaderRule.reader
+        assertNotNull(reader.poll(TEST_TIMEOUT_MS, filter), "Original query not found")
+        if (waitTimeForNoRetryAfterCancellationMs > 0) {
+            assertNull(reader.poll(waitTimeForNoRetryAfterCancellationMs, filter),
+                "Expected no retry query")
+        } else {
+            assertNotNull(reader.poll(TEST_TIMEOUT_MS, filter), "Retry query not found")
+        }
+        return SystemClock.elapsedRealtime() - queryStart
+    }
+
+    @SetFeatureFlagsRule.FeatureFlag("no_retry_after_cancel", true)
+    @Test
+    fun testCancellation() {
+        val timeWithRetryWhenNotCancelled = doCancellationTest("test1.example.com",
+            waitTimeForNoRetryAfterCancellationMs = 0L)
+        doCancellationTest("test2.example.com",
+            waitTimeForNoRetryAfterCancellationMs = timeWithRetryWhenNotCancelled + 50L)
+    }
+}
\ No newline at end of file
diff --git a/tests/cts/net/src/android/net/cts/DnsResolverTest.java b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
index fa44ae9..b66b853 100644
--- a/tests/cts/net/src/android/net/cts/DnsResolverTest.java
+++ b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
@@ -58,6 +58,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.net.module.util.DnsPacket;
+import com.android.testutils.ConnectivityDiagnosticsCollector;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DeviceConfigRule;
@@ -394,7 +395,22 @@
     @Test
     @DnsResolverModuleTest
     public void testRawQueryNXDomainWithPrivateDns() throws Exception {
-        doTestRawQueryNXDomainWithPrivateDns(mExecutor);
+        try {
+            doTestRawQueryNXDomainWithPrivateDns(mExecutor);
+        } catch (Throwable e) {
+            final ConnectivityDiagnosticsCollector collector =
+                    ConnectivityDiagnosticsCollector.getInstance();
+            if (collector != null) {
+                // IWLAN on U QPR3 release may cause failures in this test, see
+                // CarrierConfigSetupTest which is supposed to avoid the issue. Collect IWLAN
+                // related dumpsys if the test still fails.
+                collector.collectDumpsys("carrier_config", e);
+                collector.collectDumpsys("telecom", e);
+                collector.collectDumpsys("telephony_ims", e);
+                collector.collectDumpsys("telephony.registry", e);
+            }
+            throw e;
+        }
     }
 
     @Test
diff --git a/tests/cts/net/src/android/net/cts/DnsTest.java b/tests/cts/net/src/android/net/cts/DnsTest.java
index b1e5680..e367b7d 100644
--- a/tests/cts/net/src/android/net/cts/DnsTest.java
+++ b/tests/cts/net/src/android/net/cts/DnsTest.java
@@ -18,6 +18,7 @@
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeFalse;
 
 import android.content.Context;
 import android.content.pm.PackageManager;
@@ -26,6 +27,7 @@
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkInfo;
+import android.os.Build;
 import android.os.SystemClock;
 import android.util.Log;
 
@@ -299,6 +301,8 @@
     }
 
     private void ensureIpv6Connectivity() throws InterruptedException {
+        assumeFalse(Build.MODEL.contains("Cuttlefish"));
+
         CountDownLatch latch = new CountDownLatch(1);
         final int TIMEOUT_MS = 5_000;
 
diff --git a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
index 1de4cf9..ceccf0b 100644
--- a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
+++ b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
@@ -44,6 +44,7 @@
 import android.net.RouteInfo
 import android.net.TestNetworkInterface
 import android.net.TestNetworkManager
+import android.net.TestNetworkManager.TestInterfaceRequest
 import android.net.cts.util.CtsNetUtils.TestNetworkCallback
 import android.os.HandlerThread
 import android.os.SystemClock
@@ -164,7 +165,11 @@
 
             // Only statically configure the IPv4 address; for IPv6, use the SLAAC generated
             // address.
-            iface = tnm.createTapInterface(arrayOf(LinkAddress(LOCAL_IPV4_ADDRESS, IP4_PREFIX_LEN)))
+            val req = TestInterfaceRequest.Builder()
+                    .setTap()
+                    .addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, IP4_PREFIX_LEN))
+                    .build()
+            iface = tnm.createTestInterface(req)
             assertNotNull(iface)
         }
 
diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
index 9be579b..9f32132 100644
--- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
@@ -51,6 +51,7 @@
 import android.net.StaticIpConfiguration
 import android.net.TestNetworkInterface
 import android.net.TestNetworkManager
+import android.net.TestNetworkManager.TestInterfaceRequest
 import android.net.cts.EthernetManagerTest.EthernetStateListener.CallbackEntry.EthernetStateChanged
 import android.net.cts.EthernetManagerTest.EthernetStateListener.CallbackEntry.InterfaceStateChanged
 import android.os.Build
@@ -77,6 +78,7 @@
 import com.android.testutils.assertThrows
 import com.android.testutils.runAsShell
 import com.android.testutils.waitForIdle
+import com.google.common.truth.Truth.assertThat
 import java.io.IOException
 import java.net.Inet6Address
 import java.net.Socket
@@ -168,7 +170,12 @@
                 // false, it is subsequently disabled. This means that the interface may briefly get
                 // link. With IPv6 provisioning delays (RS delay and DAD) disabled, this can cause
                 // tests that expect no network to come up when hasCarrier is false to become flaky.
-                tnm.createTapInterface(hasCarrier, false /* bringUp */)
+                val req = TestInterfaceRequest.Builder()
+                        .setTap()
+                        .setHasCarrier(hasCarrier)
+                        .setBringUp(false)
+                        .build()
+                tnm.createTestInterface(req)
             }
             val mtu = tapInterface.mtu
             packetReader = PollPacketReader(
@@ -604,6 +611,9 @@
     }
 
     private fun assumeNoInterfaceForTetheringAvailable() {
+         // Requesting a tethered interface will stop IpClient. Prevent it from doing so
+         // if adb is connected over ethernet.
+         assumeFalse(isAdbOverEthernet())
         // Interfaces that have configured NetworkCapabilities will never be used for tethering,
         // see aosp/2123900.
         try {
@@ -1068,6 +1078,9 @@
 
     @Test
     fun testSetTetheringInterfaceMode_disableEnableEthernet() {
+        // do not run this test if an interface that can be used for tethering already exists.
+        assumeNoInterfaceForTetheringAvailable()
+
         val listener = EthernetStateListener()
         addInterfaceStateListener(listener)
 
@@ -1089,4 +1102,24 @@
         setEthernetEnabled(true)
         listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_CLIENT)
     }
+
+    @Test
+    fun testGetInterfaceList_disableEnableEthernet() {
+        // Test that interface list can be obtained when ethernet is disabled.
+        setEthernetEnabled(false)
+        // Create two test interfaces and check the return list contains the interface names.
+        val iface1 = createInterface()
+        val iface2 = createInterface()
+        var ifaces = em.getInterfaceList()
+        assertThat(ifaces).containsAtLeast(iface1.name, iface2.name)
+
+        // Remove one existing test interface and check the return list doesn't contain the
+        // removed interface name.
+        removeInterface(iface1)
+        ifaces = em.getInterfaceList()
+        assertThat(ifaces).doesNotContain(iface1.name)
+        assertThat(ifaces).contains(iface2.name)
+
+        removeInterface(iface2)
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
index e94d94f..4a21f09 100644
--- a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
@@ -375,6 +375,7 @@
     }
 
     private static boolean isIpv6UdpEncapSupportedByKernel() {
+        if (SdkLevel.isAtLeastB() && isKernelVersionAtLeast("5.10.0")) return true;
         return isKernelVersionAtLeast("5.15.31")
                 || (isKernelVersionAtLeast("5.10.108") && !isKernelVersionAtLeast("5.15.0"));
     }
@@ -390,8 +391,8 @@
         assumeTrue("Not supported by kernel", isIpv6UdpEncapSupportedByKernel());
     }
 
-    // TODO: b/319532485 Figure out whether to support x86_32
     private static boolean isRequestTransformStateSupportedByKernel() {
+        if (SdkLevel.isAtLeastB()) return true;
         return NetworkUtils.isKernel64Bit() || !NetworkUtils.isKernelX86();
     }
 
diff --git a/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt b/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt
new file mode 100644
index 0000000..484cce8
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts
+
+import android.net.L2capNetworkSpecifier
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_6LOWPAN
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_ANY
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_NONE
+import android.net.L2capNetworkSpecifier.PSM_ANY
+import android.net.L2capNetworkSpecifier.ROLE_CLIENT
+import android.net.L2capNetworkSpecifier.ROLE_SERVER
+import android.net.MacAddress
+import android.os.Build
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.assertParcelingIsLossless
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@ConnectivityModuleTest
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class L2capNetworkSpecifierTest {
+    @Test
+    fun testParcelUnparcel() {
+        val remoteMac = MacAddress.fromString("01:02:03:04:05:06")
+        val specifier = L2capNetworkSpecifier.Builder()
+                .setRole(ROLE_CLIENT)
+                .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+                .setPsm(42)
+                .setRemoteAddress(remoteMac)
+                .build()
+        assertParcelingIsLossless(specifier)
+    }
+
+    @Test
+    fun testGetters() {
+        val remoteMac = MacAddress.fromString("11:22:33:44:55:66")
+        val specifier = L2capNetworkSpecifier.Builder()
+                .setRole(ROLE_CLIENT)
+                .setHeaderCompression(HEADER_COMPRESSION_NONE)
+                .setPsm(123)
+                .setRemoteAddress(remoteMac)
+                .build()
+        assertEquals(ROLE_CLIENT, specifier.getRole())
+        assertEquals(HEADER_COMPRESSION_NONE, specifier.getHeaderCompression())
+        assertEquals(123, specifier.getPsm())
+        assertEquals(remoteMac, specifier.getRemoteAddress())
+    }
+
+    @Test
+    fun testCanBeSatisfiedBy() {
+        val blanketOffer = L2capNetworkSpecifier.Builder()
+                .setRole(ROLE_SERVER)
+                .setHeaderCompression(HEADER_COMPRESSION_ANY)
+                .setPsm(PSM_ANY)
+                .build()
+
+        val reservedOffer = L2capNetworkSpecifier.Builder()
+                .setRole(ROLE_SERVER)
+                .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+                .setPsm(42)
+                .build()
+
+        val clientOffer = L2capNetworkSpecifier.Builder()
+                .setRole(ROLE_CLIENT)
+                .setHeaderCompression(HEADER_COMPRESSION_ANY)
+                .build()
+
+        val serverReservation = L2capNetworkSpecifier.Builder()
+                .setRole(ROLE_SERVER)
+                .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+                .build()
+
+        assertTrue(serverReservation.canBeSatisfiedBy(blanketOffer))
+        assertTrue(serverReservation.canBeSatisfiedBy(reservedOffer))
+        // Note: serverReservation can be filed using reserveNetwork, or it could be a regular
+        // request filed using requestNetwork.
+        assertFalse(serverReservation.canBeSatisfiedBy(clientOffer))
+
+        val clientRequest = L2capNetworkSpecifier.Builder()
+                .setRole(ROLE_CLIENT)
+                .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+                .setRemoteAddress(MacAddress.fromString("00:01:02:03:04:05"))
+                .setPsm(42)
+                .build()
+
+        assertTrue(clientRequest.canBeSatisfiedBy(clientOffer))
+        // Note: the BlanketOffer also includes a RES_ID_MATCH_ALL_RESERVATIONS. Since the
+        // clientRequest is not a reservation, it won't match that request to begin with.
+        assertFalse(clientRequest.canBeSatisfiedBy(blanketOffer))
+        assertFalse(clientRequest.canBeSatisfiedBy(reservedOffer))
+
+        val matchAny = L2capNetworkSpecifier.Builder().build()
+        assertTrue(matchAny.canBeSatisfiedBy(blanketOffer))
+        assertTrue(matchAny.canBeSatisfiedBy(reservedOffer))
+        assertTrue(matchAny.canBeSatisfiedBy(clientOffer))
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index 815c3a5..8fcc703 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -117,13 +117,13 @@
 import com.android.testutils.ConnectivityModuleTest
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.PollPacketReader
 import com.android.testutils.RecorderCallback.CallbackEntry.Available
 import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatus
 import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
 import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
 import com.android.testutils.RecorderCallback.CallbackEntry.Losing
 import com.android.testutils.RecorderCallback.CallbackEntry.Lost
-import com.android.testutils.PollPacketReader
 import com.android.testutils.TestableNetworkAgent
 import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnAddKeepalivePacketFilter
 import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnAutomaticReconnectDisabled
@@ -140,6 +140,7 @@
 import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnValidationStatus
 import com.android.testutils.TestableNetworkCallback
 import com.android.testutils.assertThrows
+import com.android.testutils.com.android.testutils.CarrierConfigRule
 import com.android.testutils.runAsShell
 import com.android.testutils.tryTest
 import com.android.testutils.waitForIdle
@@ -149,8 +150,8 @@
 import java.net.InetAddress
 import java.net.InetSocketAddress
 import java.net.Socket
-import java.security.MessageDigest
 import java.nio.ByteBuffer
+import java.security.MessageDigest
 import java.time.Duration
 import java.util.Arrays
 import java.util.Random
@@ -167,6 +168,7 @@
 import org.junit.After
 import org.junit.Assume.assumeTrue
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.any
@@ -178,10 +180,12 @@
 import org.mockito.Mockito.verify
 
 private const val TAG = "NetworkAgentTest"
+
 // This test doesn't really have a constraint on how fast the methods should return. If it's
 // going to fail, it will simply wait forever, so setting a high timeout lowers the flake ratio
 // without affecting the run time of successful runs. Thus, set a very high timeout.
 private const val DEFAULT_TIMEOUT_MS = 5000L
+
 // 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
@@ -223,6 +227,9 @@
 @IgnoreUpTo(Build.VERSION_CODES.R)
 @RunWith(DevSdkIgnoreRunner::class)
 class NetworkAgentTest {
+    @get:Rule
+    val carrierConfigRule = CarrierConfigRule()
+
     private val LOCAL_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1")
     private val REMOTE_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.2")
 
@@ -378,8 +385,12 @@
         // Ensure this NetworkAgent is never unneeded by filing a request with its specifier.
         requestNetwork(makeTestNetworkRequest(specifier), callback)
         val nc = makeTestNetworkCapabilities(specifier, transports)
-        val agent = createNetworkAgent(context, initialConfig = initialConfig, initialLp = lp,
-            initialNc = nc)
+        val agent = createNetworkAgent(
+            context,
+            initialConfig = initialConfig,
+            initialLp = lp,
+            initialNc = nc
+        )
         agent.setTeardownDelayMillis(0)
         // Connect the agent and verify initial status callbacks.
         agent.register()
@@ -414,7 +425,8 @@
 
     private fun createTunInterface(addrs: Collection<LinkAddress> = emptyList()):
             TestNetworkInterface = realContext.getSystemService(
-                TestNetworkManager::class.java)!!.createTunInterface(addrs).also {
+                TestNetworkManager::class.java
+            )!!.createTunInterface(addrs).also {
             ifacesToCleanUp.add(it)
     }
 
@@ -546,9 +558,12 @@
     @Test
     fun testSocketKeepalive(): Unit = createNetworkAgentWithFakeCS().let { agent ->
         val packet = NattKeepalivePacketData(
-                LOCAL_IPV4_ADDRESS /* srcAddress */, 1234 /* srcPort */,
-                REMOTE_IPV4_ADDRESS /* dstAddress */, 4567 /* dstPort */,
-                ByteArray(100 /* size */))
+            LOCAL_IPV4_ADDRESS /* srcAddress */,
+            1234 /* srcPort */,
+            REMOTE_IPV4_ADDRESS /* dstAddress */,
+            4567 /* dstPort */,
+            ByteArray(100 /* size */)
+        )
         val slot = 4
         val interval = 37
 
@@ -653,8 +668,13 @@
             uid: Int,
             expectUidsPresent: Boolean
     ) {
-        doTestAllowedUids(intArrayOf(transport), uid, expectUidsPresent,
-                specifier = null, transportInfo = null)
+        doTestAllowedUids(
+            intArrayOf(transport),
+            uid,
+            expectUidsPresent,
+            specifier = null,
+            transportInfo = null
+        )
     }
 
     private fun doTestAllowedUidsWithSubId(
@@ -689,21 +709,28 @@
 
     private fun setHoldCarrierPrivilege(hold: Boolean, subId: Int) {
         fun getCertHash(): String {
-            val pkgInfo = realContext.packageManager.getPackageInfo(realContext.opPackageName,
-                    PackageManager.GET_SIGNATURES)
+            val pkgInfo = realContext.packageManager.getPackageInfo(
+                realContext.opPackageName,
+                PackageManager.GET_SIGNATURES
+            )
             val digest = MessageDigest.getInstance("SHA-256")
             val certHash = digest.digest(pkgInfo.signatures!![0]!!.toByteArray())
             return UiccUtil.bytesToHexString(certHash)!!
         }
 
         val tm = realContext.getSystemService(TelephonyManager::class.java)!!
-        val ccm = realContext.getSystemService(CarrierConfigManager::class.java)!!
 
         val cv = ConditionVariable()
         val cpb = PrivilegeWaiterCallback(cv)
-        tryTest {
+        // The lambda below is capturing |cpb|, whose type inherits from a class that appeared in
+        // T. This means the lambda will compile as a private method of this class taking a
+        // PrivilegeWaiterCallback argument. As JUnit uses reflection to enumerate all methods
+        // including private methods, this would fail with a link error when running on S-.
+        // To solve this, make the lambda serializable, which causes the compiler to emit a
+        // synthetic class instead of a synthetic method.
+        tryTest @JvmSerializableLambda {
             val slotIndex = SubscriptionManager.getSlotIndex(subId)!!
-            runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+            runAsShell(READ_PRIVILEGED_PHONE_STATE) @JvmSerializableLambda {
                 tm.registerCarrierPrivilegesCallback(slotIndex, { it.run() }, cpb)
             }
             // Wait for the callback to be registered
@@ -716,21 +743,16 @@
                 }
                 return@tryTest
             }
-            cv.close()
-            runAsShell(MODIFY_PHONE_STATE) {
-                val carrierConfigs = if (hold) {
-                    PersistableBundle().also {
-                        it.putStringArray(CarrierConfigManager.KEY_CARRIER_CERTIFICATE_STRING_ARRAY,
-                                arrayOf(getCertHash()))
-                    }
-                } else {
-                    null
-                }
-                ccm.overrideConfig(subId, carrierConfigs)
+            if (hold) {
+                carrierConfigRule.addConfigOverrides(subId, PersistableBundle().also {
+                    it.putStringArray(CarrierConfigManager.KEY_CARRIER_CERTIFICATE_STRING_ARRAY,
+                        arrayOf(getCertHash()))
+                })
+            } else {
+                carrierConfigRule.cleanUpNow()
             }
-            assertTrue(cv.block(DEFAULT_TIMEOUT_MS), "Can't change carrier privilege")
-        } cleanup {
-            runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+        } cleanup @JvmSerializableLambda {
+            runAsShell(READ_PRIVILEGED_PHONE_STATE) @JvmSerializableLambda {
                 tm.unregisterCarrierPrivilegesCallback(cpb)
             }
         }
@@ -744,9 +766,15 @@
 
         val cv = ConditionVariable()
         val cpb = CarrierServiceChangedWaiterCallback(cv)
-        tryTest {
+        // The lambda below is capturing |cpb|, whose type inherits from a class that appeared in
+        // T. This means the lambda will compile as a private method of this class taking a
+        // PrivilegeWaiterCallback argument. As JUnit uses reflection to enumerate all methods
+        // including private methods, this would fail with a link error when running on S-.
+        // To solve this, make the lambda serializable, which causes the compiler to emit a
+        // synthetic class instead of a synthetic method.
+        tryTest @JvmSerializableLambda {
             val slotIndex = SubscriptionManager.getSlotIndex(subId)!!
-            runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+            runAsShell(READ_PRIVILEGED_PHONE_STATE) @JvmSerializableLambda {
                 tm.registerCarrierPrivilegesCallback(slotIndex, { it.run() }, cpb)
             }
             // Wait for the callback to be registered
@@ -768,8 +796,8 @@
                 }
             }
             assertTrue(cv.block(DEFAULT_TIMEOUT_MS), "Can't modify carrier service package")
-        } cleanup {
-            runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+        } cleanup @JvmSerializableLambda {
+            runAsShell(READ_PRIVILEGED_PHONE_STATE) @JvmSerializableLambda {
                 tm.unregisterCarrierPrivilegesCallback(cpb)
             }
         }
@@ -799,14 +827,19 @@
         val uid = try {
             realContext.packageManager.getApplicationInfo(servicePackage, 0).uid
         } catch (e: PackageManager.NameNotFoundException) {
-            fail("$servicePackage could not be installed, please check the SuiteApkInstaller" +
-                    " installed CtsCarrierServicePackage.apk", e)
+            fail(
+                "$servicePackage could not be installed, please check the SuiteApkInstaller" +
+                    " installed CtsCarrierServicePackage.apk",
+                e
+            )
         }
 
         val tm = realContext.getSystemService(TelephonyManager::class.java)!!
         val defaultSubId = SubscriptionManager.getDefaultSubscriptionId()
-        assertTrue(defaultSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID,
-                "getDefaultSubscriptionId returns INVALID_SUBSCRIPTION_ID")
+        assertTrue(
+            defaultSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID,
+            "getDefaultSubscriptionId returns INVALID_SUBSCRIPTION_ID"
+        )
         tryTest {
             // This process is not the carrier service UID, so allowedUids should be ignored in all
             // the following cases.
@@ -910,8 +943,10 @@
         // If using the int ranking, agent1 must be upgraded to a better score so that there is
         // no ambiguity when agent2 connects that agent1 is still better. If using policy
         // ranking, this is not necessary.
-        agent1.sendNetworkScore(NetworkScore.Builder().setLegacyInt(BETTER_NETWORK_SCORE)
-                .build())
+        agent1.sendNetworkScore(
+            NetworkScore.Builder().setLegacyInt(BETTER_NETWORK_SCORE)
+                .build()
+        )
 
         // Connect the second agent.
         val (agent2, _) = createConnectedNetworkAgent()
@@ -920,10 +955,12 @@
         // virtue of already satisfying the request.
         callback.assertNoCallback(NO_CALLBACK_TIMEOUT)
         // Now downgrade the score and expect the callback now prefers agent2
-        agent1.sendNetworkScore(NetworkScore.Builder()
+        agent1.sendNetworkScore(
+            NetworkScore.Builder()
                 .setLegacyInt(WORSE_NETWORK_SCORE)
                 .setExiting(true)
-                .build())
+                .build()
+        )
         callback.expect<Available>(agent2.network!!)
 
         // tearDown() will unregister the requests and agents
@@ -968,16 +1005,20 @@
         // Check that the default network's transport is propagated to the VPN.
         var vpnNc = mCM.getNetworkCapabilities(agent.network!!)
         assertNotNull(vpnNc)
-        assertEquals(VpnManager.TYPE_VPN_SERVICE,
-                (vpnNc.transportInfo as VpnTransportInfo).type)
+        assertEquals(
+            VpnManager.TYPE_VPN_SERVICE,
+            (vpnNc.transportInfo as VpnTransportInfo).type
+        )
         assertEquals(mySessionId, (vpnNc.transportInfo as VpnTransportInfo).sessionId)
 
         val testAndVpn = intArrayOf(TRANSPORT_TEST, TRANSPORT_VPN)
         assertTrue(vpnNc.hasAllTransports(testAndVpn))
         assertFalse(vpnNc.hasCapability(NET_CAPABILITY_NOT_VPN))
-        assertTrue(vpnNc.hasAllTransports(defaultNetworkTransports),
-                "VPN transports ${Arrays.toString(vpnNc.transportTypes)}" +
-                        " lacking transports from ${Arrays.toString(defaultNetworkTransports)}")
+        assertTrue(
+            vpnNc.hasAllTransports(defaultNetworkTransports),
+            "VPN transports ${Arrays.toString(vpnNc.transportTypes)}" +
+                    " lacking transports from ${Arrays.toString(defaultNetworkTransports)}"
+        )
 
         // Check that when no underlying networks are announced the underlying transport disappears.
         agent.setUnderlyingNetworks(listOf<Network>())
@@ -999,9 +1040,11 @@
         // underlying networks, and because not congested, not roaming, and not suspended are the
         // default anyway. It's still useful as an extra check though.
         vpnNc = mCM.getNetworkCapabilities(agent.network!!)!!
-        for (cap in listOf(NET_CAPABILITY_NOT_CONGESTED,
-                NET_CAPABILITY_NOT_ROAMING,
-                NET_CAPABILITY_NOT_SUSPENDED)) {
+        for (cap in listOf(
+            NET_CAPABILITY_NOT_CONGESTED,
+            NET_CAPABILITY_NOT_ROAMING,
+            NET_CAPABILITY_NOT_SUSPENDED
+        )) {
             val capStr = valueToString(NetworkCapabilities::class.java, "NET_CAPABILITY_", cap)
             if (defaultNetworkCapabilities.hasCapability(cap) && !vpnNc.hasCapability(cap)) {
                 fail("$capStr not propagated from underlying: $defaultNetworkCapabilities")
@@ -1026,13 +1069,15 @@
         doReturn(mockCm).`when`(mockContext).getSystemService(Context.CONNECTIVITY_SERVICE)
         val agent = createNetworkAgent(mockContext)
         agent.register()
-        verify(mockCm).registerNetworkAgent(any(),
-                argThat<NetworkInfo> { it.detailedState == NetworkInfo.DetailedState.CONNECTING },
-                any(LinkProperties::class.java),
-                any(NetworkCapabilities::class.java),
-                any(NetworkScore::class.java),
-                any(NetworkAgentConfig::class.java),
-                eq(NetworkProvider.ID_NONE))
+        verify(mockCm).registerNetworkAgent(
+            any(),
+            argThat<NetworkInfo> { it.detailedState == NetworkInfo.DetailedState.CONNECTING },
+            any(LinkProperties::class.java),
+            any(NetworkCapabilities::class.java),
+            any(NetworkScore::class.java),
+            any(NetworkAgentConfig::class.java),
+            eq(NetworkProvider.ID_NONE)
+        )
     }
 
     @Test
@@ -1079,8 +1124,10 @@
     @Test
     fun testValidationStatus() = createNetworkAgentWithFakeCS().let { agent ->
         val uri = Uri.parse("http://www.google.com")
-        mFakeConnectivityService.agent.onValidationStatusChanged(VALID_NETWORK,
-                uri.toString())
+        mFakeConnectivityService.agent.onValidationStatusChanged(
+            VALID_NETWORK,
+            uri.toString()
+        )
         agent.expectCallback<OnValidationStatus>().let {
             assertEquals(it.status, VALID_NETWORK)
             assertEquals(it.uri, uri)
@@ -1155,7 +1202,8 @@
         }
         assertFailsWith<IllegalArgumentException> {
             agentWeaker.setLingerDuration(Duration.ofMillis(
-                    NetworkAgent.MIN_LINGER_TIMER_MS.toLong() - 1))
+                    NetworkAgent.MIN_LINGER_TIMER_MS.toLong() - 1
+            ))
         }
         // Verify valid linger timer can be set, but it should not take effect since the network
         // is still needed.
@@ -1165,11 +1213,14 @@
         agentWeaker.setLingerDuration(Duration.ofMillis(NetworkAgent.MIN_LINGER_TIMER_MS.toLong()))
         // Make a listener which can observe agentWeaker lost later.
         val callbackWeaker = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
-        registerNetworkCallback(NetworkRequest.Builder()
+        registerNetworkCallback(
+            NetworkRequest.Builder()
                 .clearCapabilities()
                 .addTransportType(TRANSPORT_TEST)
                 .setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(specifierWeaker))
-                .build(), callbackWeaker)
+                .build(),
+            callbackWeaker
+        )
         callbackWeaker.expectAvailableCallbacks(agentWeaker.network!!)
 
         // Connect the agentStronger with a score better than agentWeaker. Verify the callback for
@@ -1186,8 +1237,10 @@
         val expectedRemainingLingerDuration = lingerStart +
                 NetworkAgent.MIN_LINGER_TIMER_MS.toLong() - SystemClock.elapsedRealtime()
         // If the available callback is too late. The remaining duration will be reduced.
-        assertTrue(expectedRemainingLingerDuration > 0,
-                "expected remaining linger duration is $expectedRemainingLingerDuration")
+        assertTrue(
+            expectedRemainingLingerDuration > 0,
+            "expected remaining linger duration is $expectedRemainingLingerDuration"
+        )
         callbackWeaker.assertNoCallback(expectedRemainingLingerDuration)
         callbackWeaker.expect<Lost>(agentWeaker.network!!)
     }
@@ -1205,20 +1258,24 @@
         assertEquals(imsi, testNetworkSnapshot!!.subscriberId)
     }
 
-    @Test
-    @IgnoreUpTo(Build.VERSION_CODES.R)
     // TODO: Refactor helper functions to util class and move this test case to
     //  {@link android.net.cts.ConnectivityManagerTest}.
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)
     fun testRegisterBestMatchingNetworkCallback() {
         // Register best matching network callback with additional condition that will be
         // exercised later. This assumes the test network agent has NOT_VCN_MANAGED in it and
         // does not have NET_CAPABILITY_TEMPORARILY_NOT_METERED.
         val bestMatchingCb = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
-        registerBestMatchingNetworkCallback(NetworkRequest.Builder()
+        registerBestMatchingNetworkCallback(
+            NetworkRequest.Builder()
                 .clearCapabilities()
                 .addTransportType(TRANSPORT_TEST)
                 .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
-                .build(), bestMatchingCb, mHandlerThread.threadHandler)
+                .build(),
+            bestMatchingCb,
+            mHandlerThread.threadHandler
+        )
 
         val (agent1, _) = createConnectedNetworkAgent(specifier = "AGENT-1")
         bestMatchingCb.expectAvailableThenValidatedCallbacks(agent1.network!!)
@@ -1296,8 +1353,10 @@
         }
 
         fun assertNoCallback() {
-            assertNull(history.poll(NO_CALLBACK_TIMEOUT),
-                    "Callback received")
+            assertNull(
+                history.poll(NO_CALLBACK_TIMEOUT),
+                "Callback received"
+            )
         }
     }
 
@@ -1312,7 +1371,8 @@
 
     private fun setupForQosDatagram() = setupForQosCallbackTest {
         agent: TestableNetworkAgent -> DatagramSocket(
-            InetSocketAddress(InetAddress.getLoopbackAddress(), 0))
+            InetSocketAddress(InetAddress.getLoopbackAddress(), 0)
+        )
             .also { assertNotNull(agent.network?.bindSocket(it)) }
     }
 
@@ -1345,7 +1405,8 @@
 
                 assertFailsWith<QosCallbackRegistrationException>(
                         "The same callback cannot be " +
-                        "registered more than once without first being unregistered") {
+                        "registered more than once without first being unregistered"
+                ) {
                     mCM.registerQosCallback(info, executor, qosCallback)
                 }
             } finally {
@@ -1438,8 +1499,10 @@
                 qosCallback.expectCallback<OnQosSessionAvailable>()
 
                 // Check that onError is coming through correctly
-                agent.sendQosCallbackError(callbackId,
-                        QosCallbackException.EX_TYPE_FILTER_NOT_SUPPORTED)
+                agent.sendQosCallbackError(
+                    callbackId,
+                    QosCallbackException.EX_TYPE_FILTER_NOT_SUPPORTED
+                )
                 qosCallback.expectCallback<OnError> {
                     it.ex.cause is UnsupportedOperationException
                 }
@@ -1534,13 +1597,20 @@
         val remoteAddresses = ArrayList<InetSocketAddress>()
         remoteAddresses.add(InetSocketAddress(REMOTE_ADDRESS, 80))
         return EpsBearerQosSessionAttributes(
-                qci, 2, 3, 4, 5,
-                remoteAddresses
+            qci,
+            2,
+            3,
+            4,
+            5,
+            remoteAddresses
         )
     }
 
-    fun sendAndExpectUdpPacket(net: Network,
-                               reader: PollPacketReader, iface: TestNetworkInterface) {
+    fun sendAndExpectUdpPacket(
+        net: Network,
+        reader: PollPacketReader,
+        iface: TestNetworkInterface
+    ) {
         val s = Os.socket(AF_INET6, SOCK_DGRAM, 0)
         net.bindSocket(s)
         val content = ByteArray(16)
@@ -1553,8 +1623,11 @@
                     it[IPV6_PROTOCOL_OFFSET].toInt() == IPPROTO_UDP &&
                     Arrays.equals(content, it.copyOfRange(udpStart, udpStart + content.size))
         }
-        assertNotNull(match, "Did not receive matching packet on ${iface.interfaceName} " +
-                " after ${DEFAULT_TIMEOUT_MS}ms")
+        assertNotNull(
+            match,
+            "Did not receive matching packet on ${iface.interfaceName} " +
+                " after ${DEFAULT_TIMEOUT_MS}ms"
+        )
     }
 
     fun createInterfaceAndReader(): Triple<TestNetworkInterface, PollPacketReader, LinkProperties> {
@@ -1750,8 +1823,10 @@
         assertNotNull(wifiSpecifier)
         assertTrue(wifiSpecifier is EthernetNetworkSpecifier)
 
-        val wifiNc = makeTestNetworkCapabilities(wifiSpecifier.interfaceName,
-                intArrayOf(TRANSPORT_WIFI))
+        val wifiNc = makeTestNetworkCapabilities(
+            wifiSpecifier.interfaceName,
+            intArrayOf(TRANSPORT_WIFI)
+        )
         wifiAgent.sendNetworkCapabilities(wifiNc)
         val wifiLp = mCM.getLinkProperties(wifiNetwork)!!
         val newRoute = RouteInfo(IpPrefix("192.0.2.42/24"))
@@ -1822,8 +1897,12 @@
         val nc = makeTestNetworkCapabilities(ifName, transports).also {
             if (transports.contains(TRANSPORT_VPN)) {
                 val sessionId = "NetworkAgentTest-${Process.myPid()}"
-                it.setTransportInfo(VpnTransportInfo(VpnManager.TYPE_VPN_PLATFORM, sessionId,
-                    /*bypassable=*/ false, /*longLivedTcpConnectionsExpensive=*/ false))
+                it.setTransportInfo(VpnTransportInfo(
+                    VpnManager.TYPE_VPN_PLATFORM,
+                    sessionId,
+                    /*bypassable=*/ false,
+                    /*longLivedTcpConnectionsExpensive=*/ false
+                ))
                 it.underlyingNetworks = listOf()
             }
         }
@@ -1868,9 +1947,11 @@
         listenCallback.expect<Available>(network)
 
         requestCallback.expect<CapabilitiesChanged>(network) { it.caps.hasCapability(
-            NET_CAPABILITY_TEMPORARILY_NOT_METERED) }
+            NET_CAPABILITY_TEMPORARILY_NOT_METERED
+        ) }
         listenCallback.expect<CapabilitiesChanged>(network) { it.caps.hasCapability(
-            NET_CAPABILITY_TEMPORARILY_NOT_METERED) }
+            NET_CAPABILITY_TEMPORARILY_NOT_METERED
+        ) }
 
         requestCallback.expect<LinkPropertiesChanged>(network) { it.lp.equals(lp) }
         listenCallback.expect<LinkPropertiesChanged>(network) { it.lp.equals(lp) }
@@ -1896,7 +1977,8 @@
     fun testNativeNetworkCreation_PhysicalNetwork() {
         doTestNativeNetworkCreation(
                 expectCreatedImmediately = SHOULD_CREATE_NETWORKS_IMMEDIATELY,
-                intArrayOf(TRANSPORT_CELLULAR))
+                intArrayOf(TRANSPORT_CELLULAR)
+        )
     }
 
     @Test
diff --git a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
index ff10e1a..2fb140a 100644
--- a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
@@ -16,6 +16,7 @@
 
 package android.net.cts;
 
+import static android.net.ConnectivityManager.TYPE_NONE;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_FOTA;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
@@ -32,6 +33,7 @@
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
 import static com.google.common.truth.Truth.assertThat;
 
 import static junit.framework.Assert.fail;
@@ -41,6 +43,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
@@ -101,6 +104,16 @@
         }
     }
 
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
+    public void testParceling() {
+        NetworkCapabilities nc = new NetworkCapabilities.Builder().build();
+        NetworkRequest request = new NetworkRequest(nc, TYPE_NONE, 42 /* rId */,
+                NetworkRequest.Type.RESERVATION);
+
+        assertParcelingIsLossless(request);
+    }
+
     @Test
     public void testCapabilities() {
         assertTrue(new NetworkRequest.Builder().addCapability(NET_CAPABILITY_MMS).build()
@@ -130,7 +143,7 @@
         verifyNoCapabilities(builder.build());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testTemporarilyNotMeteredCapability() {
         assertTrue(new NetworkRequest.Builder()
                 .addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED).build()
@@ -157,7 +170,6 @@
     }
 
     @Test
-    @IgnoreUpTo(Build.VERSION_CODES.Q)
     public void testSpecifier() {
         assertNull(new NetworkRequest.Builder().build().getNetworkSpecifier());
         final WifiNetworkSpecifier specifier = new WifiNetworkSpecifier.Builder()
@@ -192,7 +204,6 @@
     }
 
     @Test
-    @IgnoreUpTo(Build.VERSION_CODES.Q)
     public void testRequestorPackageName() {
         assertNull(new NetworkRequest.Builder().build().getRequestorPackageName());
         final String pkgName = "android.net.test";
@@ -216,7 +227,6 @@
     }
 
     @Test
-    @IgnoreUpTo(Build.VERSION_CODES.Q)
     public void testCanBeSatisfiedBy() {
         final LocalNetworkSpecifier specifier1 = new LocalNetworkSpecifier(1234 /* id */);
         final LocalNetworkSpecifier specifier2 = new LocalNetworkSpecifier(5678 /* id */);
@@ -284,7 +294,6 @@
     }
 
     @Test
-    @IgnoreUpTo(Build.VERSION_CODES.Q)
     public void testInvariantInCanBeSatisfiedBy() {
         // Test invariant that result of NetworkRequest.canBeSatisfiedBy() should be the same with
         // NetworkCapabilities.satisfiedByNetworkCapabilities().
@@ -388,7 +397,7 @@
                 otherUidsRequest.canBeSatisfiedBy(ncWithOtherUid));
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testRequestorUid() {
         final NetworkCapabilities nc = new NetworkCapabilities();
         // Verify default value is INVALID_UID
@@ -558,4 +567,43 @@
                 .setBssidPattern(ARBITRARY_ADDRESS, ARBITRARY_ADDRESS)
                 .build();
     }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testNetworkReservation() {
+        final NetworkCapabilities nc = new NetworkCapabilities();
+        final NetworkCapabilities blanketOffer = new NetworkCapabilities(nc);
+        blanketOffer.setReservationId(NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS);
+        final NetworkCapabilities specificOffer = new NetworkCapabilities(nc);
+        specificOffer.setReservationId(42);
+        final NetworkCapabilities otherSpecificOffer = new NetworkCapabilities(nc);
+        otherSpecificOffer.setReservationId(43);
+        final NetworkCapabilities regularOffer = new NetworkCapabilities(nc);
+
+        final NetworkRequest reservationNR = new NetworkRequest(new NetworkCapabilities(nc),
+                TYPE_NONE, 42 /* rId */, NetworkRequest.Type.RESERVATION);
+        final NetworkRequest requestNR = new NetworkRequest(new NetworkCapabilities(nc),
+                TYPE_NONE, 42 /* rId */, NetworkRequest.Type.REQUEST);
+
+        assertTrue(reservationNR.canBeSatisfiedBy(blanketOffer));
+        assertTrue(reservationNR.canBeSatisfiedBy(specificOffer));
+        assertFalse(reservationNR.canBeSatisfiedBy(otherSpecificOffer));
+        assertFalse(reservationNR.canBeSatisfiedBy(regularOffer));
+
+        assertFalse(requestNR.canBeSatisfiedBy(blanketOffer));
+        assertTrue(requestNR.canBeSatisfiedBy(specificOffer));
+        assertTrue(requestNR.canBeSatisfiedBy(otherSpecificOffer));
+        assertTrue(requestNR.canBeSatisfiedBy(regularOffer));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testNetworkRequest_throwsWhenPassingCapsWithReservationId() {
+        final NetworkCapabilities capsWithResId = new NetworkCapabilities();
+        capsWithResId.setReservationId(42);
+
+        assertThrows(IllegalArgumentException.class, () -> {
+            new NetworkRequest(capsWithResId, TYPE_NONE, 42 /* rId */, NetworkRequest.Type.REQUEST);
+        });
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/NetworkReservationTest.kt b/tests/cts/net/src/android/net/cts/NetworkReservationTest.kt
new file mode 100644
index 0000000..a9af34f
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkReservationTest.kt
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts
+
+import android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.Manifest.permission.NETWORK_SETTINGS
+import android.net.ConnectivityManager
+import android.net.L2capNetworkSpecifier
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_6LOWPAN
+import android.net.L2capNetworkSpecifier.ROLE_SERVER
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS
+import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_TEST
+import android.net.NetworkProvider
+import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.platform.test.annotations.AppModeFull
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Reserved
+import com.android.testutils.RecorderCallback.CallbackEntry.Unavailable
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.TestableNetworkOfferCallback
+import com.android.testutils.runAsShell
+import kotlin.test.assertContains
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TAG = "NetworkReservationTest"
+
+private val NETWORK_SCORE = NetworkScore.Builder().build()
+private val ETHERNET_CAPS = NetworkCapabilities.Builder()
+        .addTransportType(TRANSPORT_ETHERNET)
+        .addTransportType(TRANSPORT_TEST)
+        .addCapability(NET_CAPABILITY_INTERNET)
+        .addCapability(NET_CAPABILITY_NOT_CONGESTED)
+        .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+        .removeCapability(NET_CAPABILITY_TRUSTED)
+        .build()
+private val BLANKET_CAPS = NetworkCapabilities(ETHERNET_CAPS).apply {
+    reservationId = RES_ID_MATCH_ALL_RESERVATIONS
+}
+private val ETHERNET_REQUEST = NetworkRequest.Builder()
+        .addTransportType(TRANSPORT_ETHERNET)
+        .addTransportType(TRANSPORT_TEST)
+        .removeCapability(NET_CAPABILITY_TRUSTED)
+        .build()
+private const val TIMEOUT_MS = 5_000L
+private const val NO_CB_TIMEOUT_MS = 200L
+
+// TODO: integrate with CSNetworkReservationTest and move to common tests.
+@AppModeFull(reason = "CHANGE_NETWORK_STATE, MANAGE_TEST_NETWORKS not grantable to instant apps")
+@ConnectivityModuleTest
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class NetworkReservationTest {
+    private val context = InstrumentationRegistry.getInstrumentation().context
+    private val cm = context.getSystemService(ConnectivityManager::class.java)!!
+    private val handlerThread = HandlerThread("$TAG handler thread").apply { start() }
+    private val handler = Handler(handlerThread.looper)
+    private val provider = NetworkProvider(context, handlerThread.looper, TAG)
+
+    private val registeredCallbacks = ArrayList<TestableNetworkCallback>()
+
+    @Before
+    fun setUp() {
+        runAsShell(NETWORK_SETTINGS) {
+            cm.registerNetworkProvider(provider)
+        }
+    }
+
+    @After
+    fun tearDown() {
+        registeredCallbacks.forEach { cm.unregisterNetworkCallback(it) }
+        runAsShell(NETWORK_SETTINGS) {
+            // unregisterNetworkProvider unregisters all associated NetworkOffers.
+            cm.unregisterNetworkProvider(provider)
+        }
+        handlerThread.quitSafely()
+        handlerThread.join()
+    }
+
+    fun NetworkCapabilities.copyWithReservationId(resId: Int) = NetworkCapabilities(this).also {
+        it.reservationId = resId
+    }
+
+    fun reserveNetwork(nr: NetworkRequest): TestableNetworkCallback {
+        return TestableNetworkCallback().also {
+            cm.reserveNetwork(nr, handler, it)
+            registeredCallbacks.add(it)
+        }
+    }
+
+    @Test
+    fun testReserveNetwork() {
+        // register blanket offer
+        val blanketOffer = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+        runAsShell(MANAGE_TEST_NETWORKS) {
+            provider.registerNetworkOffer(NETWORK_SCORE, BLANKET_CAPS, handler::post, blanketOffer)
+        }
+
+        val cb = reserveNetwork(ETHERNET_REQUEST)
+
+        // validate the reservation matches the blanket offer.
+        val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+        val reservationId = reservationReq.networkCapabilities.reservationId
+
+        // bring up reserved reservation offer
+        val reservedCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+        val reservedOffer = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+        runAsShell(MANAGE_TEST_NETWORKS) {
+            provider.registerNetworkOffer(NETWORK_SCORE, reservedCaps, handler::post, reservedOffer)
+        }
+
+        // validate onReserved was sent to the app
+        val appObservedCaps = cb.expect<Reserved>().caps
+        assertEquals(reservedCaps, appObservedCaps)
+
+        // validate the reservation matches the reserved offer.
+        reservedOffer.expectOnNetworkNeeded(reservedCaps)
+
+        // reserved offer goes away
+        provider.unregisterNetworkOffer(reservedOffer)
+        cb.expect<Unavailable>()
+    }
+
+    @Test
+    fun testReserveL2capNetwork() {
+        val l2capReservationSpecifier = L2capNetworkSpecifier.Builder()
+                .setRole(ROLE_SERVER)
+                .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+                .build()
+        val l2capRequest = NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_BLUETOOTH)
+                .removeCapability(NET_CAPABILITY_TRUSTED)
+                .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                .setNetworkSpecifier(l2capReservationSpecifier)
+                .build()
+        val cb = runAsShell(CONNECTIVITY_USE_RESTRICTED_NETWORKS) {
+            reserveNetwork(l2capRequest)
+        }
+
+        val caps = cb.expect<Reserved>().caps
+        val reservedSpec = caps.networkSpecifier
+        assertTrue(reservedSpec is L2capNetworkSpecifier)
+        assertContains(0x80..0xFF, reservedSpec.psm, "PSM is outside of dynamic range")
+        assertEquals(HEADER_COMPRESSION_6LOWPAN, reservedSpec.headerCompression)
+        assertNull(reservedSpec.remoteAddress)
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java b/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java
index 10adee0..65daf57 100644
--- a/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java
@@ -114,7 +114,7 @@
             } catch (ClassNotFoundException e) {
                 /* not vulnerable if hidden API no longer available */
                 return;
-            } catch (NoSuchMethodException e) {
+            } catch (NoSuchMethodException | NoSuchMethodError e) {
                 /* not vulnerable if hidden API no longer available */
                 return;
             } catch (RemoteException e) {
diff --git a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
index e3d7240..eb2dbf7 100644
--- a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
@@ -75,6 +75,7 @@
 import com.android.compatibility.common.util.SystemUtil;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.testutils.AutoReleaseNetworkCallbackRule;
+import com.android.testutils.ConnectivityDiagnosticsCollector;
 import com.android.testutils.ConnectivityModuleTest;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
@@ -95,8 +96,10 @@
 import java.net.UnknownHostException;
 import java.text.MessageFormat;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
@@ -711,27 +714,57 @@
         }
     }
 
-    class QueryResult {
-        public final int tag;
-        public final int state;
-        public final long total;
+    class QueryResults {
+        private static class QueryKey {
+            private final int mTag;
+            private final int mState;
 
-        QueryResult(int tag, int state, NetworkStats stats) {
-            this.tag = tag;
-            this.state = state;
-            total = getTotalAndAssertNotEmpty(stats, tag, state);
+            QueryKey(int tag, int state) {
+                this.mTag = tag;
+                this.mState = state;
+            }
+
+            @Override
+            public boolean equals(Object o) {
+                if (this == o) return true;
+                if (!(o instanceof QueryKey)) return false;
+
+                QueryKey queryKey = (QueryKey) o;
+                return mTag == queryKey.mTag && mState == queryKey.mState;
+            }
+
+            @Override
+            public int hashCode() {
+                return Objects.hash(mTag, mState);
+            }
+
+            @Override
+            public String toString() {
+                return String.format("QueryKey(tag=%s, state=%s)", tagToString(mTag),
+                        stateToString(mState));
+            }
         }
 
-        public String toString() {
-            return String.format("QueryResult(tag=%s state=%s total=%d)",
-                    tagToString(tag), stateToString(state), total);
+        private final HashMap<QueryKey, Long> mSnapshot = new HashMap<>();
+
+        public long get(int tag, int state) {
+            // Expect all results are stored before access.
+            return Objects.requireNonNull(mSnapshot.get(new QueryKey(tag, state)));
+        }
+
+        public void put(int tag, int state, long total) {
+            mSnapshot.put(new QueryKey(tag, state), total);
         }
     }
 
-    private NetworkStats getNetworkStatsForTagState(int i, int tag, int state) {
-        return mNsm.queryDetailsForUidTagState(
+    private long getTotalForTagState(int i, int tag, int state, boolean assertNotEmpty,
+            long startTime, long endTime) {
+        final NetworkStats stats = mNsm.queryDetailsForUidTagState(
                 mNetworkInterfacesToTest[i].getNetworkType(), getSubscriberId(i),
-                mStartTime, mEndTime, Process.myUid(), tag, state);
+                startTime, endTime, Process.myUid(), tag, state);
+        final long total = getTotal(stats, tag, state, assertNotEmpty, startTime, endTime);
+        stats.close();
+        return total;
     }
 
     private void assertWithinPercentage(String msg, long expected, long actual, int percentage) {
@@ -742,92 +775,103 @@
         assertTrue(msg, upperBound >= actual);
     }
 
-    private void assertAlmostNoUnexpectedTraffic(NetworkStats result, int expectedTag,
+    private void assertAlmostNoUnexpectedTraffic(long total, int expectedTag,
             int expectedState, long maxUnexpected) {
-        long total = 0;
-        NetworkStats.Bucket bucket = new NetworkStats.Bucket();
-        while (result.hasNextBucket()) {
-            assertTrue(result.getNextBucket(bucket));
-            total += bucket.getRxBytes() + bucket.getTxBytes();
-        }
         if (total <= maxUnexpected) return;
 
-        fail(String.format("More than %d bytes of traffic when querying for "
-                + "tag %s state %s. Last bucket: uid=%d tag=%s state=%s bytes=%d/%d",
-                maxUnexpected, tagToString(expectedTag), stateToString(expectedState),
-                bucket.getUid(), tagToString(bucket.getTag()), stateToString(bucket.getState()),
-                bucket.getRxBytes(), bucket.getTxBytes()));
+        fail(String.format("More than %d bytes of traffic when querying for tag %s state %s.",
+                maxUnexpected, tagToString(expectedTag), stateToString(expectedState)));
     }
 
+    @ConnectivityDiagnosticsCollector.CollectTcpdumpOnFailure
     @Test
     public void testUidTagStateDetails() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             if (!shouldTestThisNetworkType(i)) {
                 continue;
             }
-            // Relatively large tolerance to accommodate for history bucket size.
-            requestNetworkAndGenerateTraffic(i, LONG_TOLERANCE);
             setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, "allow");
-            NetworkStats result = null;
-            try {
-                int currentState = isInForeground() ? STATE_FOREGROUND : STATE_DEFAULT;
-                int otherState = (currentState == STATE_DEFAULT) ? STATE_FOREGROUND : STATE_DEFAULT;
 
-                int[] tagsWithTraffic = {NETWORK_TAG, TAG_NONE};
-                int[] statesWithTraffic = {currentState, STATE_ALL};
-                ArrayList<QueryResult> resultsWithTraffic = new ArrayList<>();
+            int currentState = isInForeground() ? STATE_FOREGROUND : STATE_DEFAULT;
+            int otherState = (currentState == STATE_DEFAULT) ? STATE_FOREGROUND : STATE_DEFAULT;
 
-                int[] statesWithNoTraffic = {otherState};
-                int[] tagsWithNoTraffic = {NETWORK_TAG + 1};
-                ArrayList<QueryResult> resultsWithNoTraffic = new ArrayList<>();
+            final List<Integer> statesWithTraffic = List.of(currentState, STATE_ALL);
+            final List<Integer> statesWithNoTraffic = List.of(otherState);
+            final ArrayList<Integer> allStates = new ArrayList<>();
+            allStates.addAll(statesWithTraffic);
+            allStates.addAll(statesWithNoTraffic);
 
-                // Expect to see traffic when querying for any combination of a tag in
-                // tagsWithTraffic and a state in statesWithTraffic.
-                for (int tag : tagsWithTraffic) {
-                    for (int state : statesWithTraffic) {
-                        result = getNetworkStatsForTagState(i, tag, state);
-                        resultsWithTraffic.add(new QueryResult(tag, state, result));
-                        result.close();
-                        result = null;
+            final List<Integer> tagsWithTraffic = List.of(NETWORK_TAG, TAG_NONE);
+            final List<Integer> tagsWithNoTraffic = List.of(NETWORK_TAG + 1);
+            final ArrayList<Integer> allTags = new ArrayList<>();
+            allTags.addAll(tagsWithTraffic);
+            allTags.addAll(tagsWithNoTraffic);
+
+            // Relatively large tolerance to accommodate for history bucket size,
+            // and covering the entire test duration.
+            final long now = System.currentTimeMillis();
+            final long startTime = now - LONG_TOLERANCE;
+            final long endTime = now + LONG_TOLERANCE;
+
+            // Collect a baseline before generating network traffic.
+            QueryResults baseline = new QueryResults();
+            final ArrayList<String> logNonEmptyBaseline = new ArrayList<>();
+            for (int tag : allTags) {
+                for (int state : allStates) {
+                    final long total = getTotalForTagState(i, tag, state, false,
+                            startTime, endTime);
+                    baseline.put(tag, state, total);
+                    if (total > 0) {
+                        logNonEmptyBaseline.add(
+                                new QueryResults.QueryKey(tag, state) + "=" + total);
                     }
                 }
-
-                // Expect that the results are within a few percentage points of each other.
-                // This is ensures that FIN retransmits after the transfer is complete don't cause
-                // the test to be flaky. The test URL currently returns just over 100k so this
-                // should not be too noisy. It also ensures that the traffic sent by the test
-                // harness, which is untagged, won't cause a failure.
-                long firstTotal = resultsWithTraffic.get(0).total;
-                for (QueryResult queryResult : resultsWithTraffic) {
-                    assertWithinPercentage(queryResult + "", firstTotal, queryResult.total, 16);
-                }
-
-                // Expect to see no traffic when querying for any tag in tagsWithNoTraffic or any
-                // state in statesWithNoTraffic.
-                for (int tag : tagsWithNoTraffic) {
-                    for (int state : statesWithTraffic) {
-                        result = getNetworkStatsForTagState(i, tag, state);
-                        assertAlmostNoUnexpectedTraffic(result, tag, state, firstTotal / 100);
-                        result.close();
-                        result = null;
-                    }
-                }
-                for (int tag : tagsWithTraffic) {
-                    for (int state : statesWithNoTraffic) {
-                        result = getNetworkStatsForTagState(i, tag, state);
-                        assertAlmostNoUnexpectedTraffic(result, tag, state, firstTotal / 100);
-                        result.close();
-                        result = null;
-                    }
-                }
-            } finally {
-                if (result != null) {
-                    result.close();
-                }
             }
+            // TODO: Remove debug log for b/368624224.
+            if (logNonEmptyBaseline.size() > 0) {
+                Log.v(LOG_TAG, "Baseline=" + logNonEmptyBaseline);
+            }
+
+            // Generate some traffic and release the network.
+            requestNetworkAndGenerateTraffic(i, LONG_TOLERANCE);
+
+            QueryResults results = new QueryResults();
+            // Collect results for all combinations of tags and states.
+            for (int tag : allTags) {
+                for (int state : allStates) {
+                    final boolean assertNotEmpty = tagsWithTraffic.contains(tag)
+                            && statesWithTraffic.contains(state);
+                    final long total = getTotalForTagState(i, tag, state, assertNotEmpty,
+                            startTime, endTime) - baseline.get(tag, state);
+                    results.put(tag, state, total);
+                }
+            }
+
+            // Expect that the results are within a few percentage points of each other.
+            // This is ensures that FIN retransmits after the transfer is complete don't cause
+            // the test to be flaky. The test URL currently returns just over 100k so this
+            // should not be too noisy. It also ensures that the traffic sent by the test
+            // harness, which is untagged, won't cause a failure.
+            long totalOfNetworkTagAndCurrentState = results.get(NETWORK_TAG, currentState);
+            for (int tag : allTags) {
+                for (int state : allStates) {
+                    final long result = results.get(tag, state);
+                    final String queryKeyStr = new QueryResults.QueryKey(tag, state).toString();
+                    if (tagsWithTraffic.contains(tag) && statesWithTraffic.contains(state)) {
+                        assertWithinPercentage(queryKeyStr,
+                                totalOfNetworkTagAndCurrentState, result, 16);
+                    } else {
+                        // Expect to see no traffic when querying for any combination with tag
+                        // in tagsWithNoTraffic or any state in statesWithNoTraffic.
+                        assertAlmostNoUnexpectedTraffic(result, tag, state,
+                                totalOfNetworkTagAndCurrentState / 100);
+                    }
+                }
+            }
+
             setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, "deny");
             try {
-                result = mNsm.queryDetailsForUidTag(
+                mNsm.queryDetailsForUidTag(
                         mNetworkInterfacesToTest[i].getNetworkType(), getSubscriberId(i),
                         mStartTime, mEndTime, Process.myUid(), NETWORK_TAG);
                 fail("negative testUidDetails fails: no exception thrown.");
@@ -900,7 +944,7 @@
         }
     }
 
-    private String tagToString(Integer tag) {
+    private static String tagToString(Integer tag) {
         if (tag == null) return "null";
         switch (tag) {
             case TAG_NONE:
@@ -910,7 +954,7 @@
         }
     }
 
-    private String stateToString(Integer state) {
+    private static String stateToString(Integer state) {
         if (state == null) return "null";
         switch (state) {
             case STATE_ALL:
@@ -923,8 +967,8 @@
         throw new IllegalArgumentException("Unknown state " + state);
     }
 
-    private long getTotalAndAssertNotEmpty(NetworkStats result, Integer expectedTag,
-            Integer expectedState) {
+    private long getTotal(NetworkStats result, Integer expectedTag,
+            Integer expectedState, boolean assertNotEmpty, long startTime, long endTime) {
         assertTrue(result != null);
         NetworkStats.Bucket bucket = new NetworkStats.Bucket();
         long totalTxPackets = 0;
@@ -933,7 +977,7 @@
         long totalRxBytes = 0;
         while (result.hasNextBucket()) {
             assertTrue(result.getNextBucket(bucket));
-            assertTimestamps(bucket);
+            assertTimestamps(bucket, startTime, endTime);
             if (expectedTag != null) assertEquals(bucket.getTag(), (int) expectedTag);
             if (expectedState != null) assertEquals(bucket.getState(), (int) expectedState);
             assertEquals(bucket.getMetered(), METERED_ALL);
@@ -949,23 +993,29 @@
         assertFalse(result.getNextBucket(bucket));
         String msg = String.format("uid %d tag %s state %s",
                 Process.myUid(), tagToString(expectedTag), stateToString(expectedState));
-        assertTrue("No Rx bytes usage for " + msg, totalRxBytes > 0);
-        assertTrue("No Rx packets usage for " + msg, totalRxPackets > 0);
-        assertTrue("No Tx bytes usage for " + msg, totalTxBytes > 0);
-        assertTrue("No Tx packets usage for " + msg, totalTxPackets > 0);
+        if (assertNotEmpty) {
+            assertTrue("No Rx bytes usage for " + msg, totalRxBytes > 0);
+            assertTrue("No Rx packets usage for " + msg, totalRxPackets > 0);
+            assertTrue("No Tx bytes usage for " + msg, totalTxBytes > 0);
+            assertTrue("No Tx packets usage for " + msg, totalTxPackets > 0);
+        }
 
         return totalRxBytes + totalTxBytes;
     }
 
     private long getTotalAndAssertNotEmpty(NetworkStats result) {
-        return getTotalAndAssertNotEmpty(result, null, STATE_ALL);
+        return getTotal(result, null, STATE_ALL, true /*assertEmpty*/, mStartTime, mEndTime);
     }
 
     private void assertTimestamps(final NetworkStats.Bucket bucket) {
+        assertTimestamps(bucket, mStartTime, mEndTime);
+    }
+
+    private void assertTimestamps(final NetworkStats.Bucket bucket, long startTime, long endTime) {
         assertTrue("Start timestamp " + bucket.getStartTimeStamp() + " is less than "
-                + mStartTime, bucket.getStartTimeStamp() >= mStartTime);
+                + startTime, bucket.getStartTimeStamp() >= startTime);
         assertTrue("End timestamp " + bucket.getEndTimeStamp() + " is greater than "
-                + mEndTime, bucket.getEndTimeStamp() <= mEndTime);
+                + endTime, bucket.getEndTimeStamp() <= endTime);
     }
 
     private static class TestUsageCallback extends NetworkStatsManager.UsageCallback {
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
index 24af42b..1973899 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
@@ -92,7 +92,7 @@
             assertEquals(downstreamIface.name, iface)
             val request = TetheringRequest.Builder(TETHERING_ETHERNET)
                 .setConnectivityScope(CONNECTIVITY_SCOPE_LOCAL).build()
-            tetheringEventCallback = enableEthernetTethering(
+            tetheringEventCallback = enableTethering(
                 iface, request,
                 null /* any upstream */
             ).apply {
@@ -125,7 +125,7 @@
             val request = TetheringRequest.Builder(TETHERING_ETHERNET)
                 .setStaticIpv4Addresses(localAddr, clientAddr)
                 .setShouldShowEntitlementUi(false).build()
-            tetheringEventCallback = enableEthernetTethering(
+            tetheringEventCallback = enableTethering(
                 iface, request,
                 null /* any upstream */
             ).apply {
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index 7fc8863..ee31f1a 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -22,14 +22,10 @@
 import android.net.ConnectivityManager.NetworkCallback
 import android.net.DnsResolver
 import android.net.InetAddresses.parseNumericAddress
-import android.net.LinkAddress
-import android.net.LinkProperties
 import android.net.LocalSocket
 import android.net.LocalSocketAddress
 import android.net.MacAddress
 import android.net.Network
-import android.net.NetworkAgentConfig
-import android.net.NetworkCapabilities
 import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED
 import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
 import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
@@ -40,8 +36,11 @@
 import android.net.TestNetworkSpecifier
 import android.net.connectivity.ConnectivityCompatChanges
 import android.net.cts.util.CtsNetUtils
+import android.net.nsd.AdvertisingRequest
+import android.net.nsd.AdvertisingRequest.FLAG_SKIP_PROBING
 import android.net.nsd.DiscoveryRequest
 import android.net.nsd.NsdManager
+import android.net.nsd.NsdManager.PROTOCOL_DNS_SD
 import android.net.nsd.NsdServiceInfo
 import android.net.nsd.OffloadEngine
 import android.net.nsd.OffloadServiceInfo
@@ -50,16 +49,10 @@
 import android.os.HandlerThread
 import android.platform.test.annotations.AppModeFull
 import android.provider.DeviceConfig.NAMESPACE_TETHERING
-import android.system.ErrnoException
-import android.system.Os
-import android.system.OsConstants.AF_INET6
-import android.system.OsConstants.EADDRNOTAVAIL
-import android.system.OsConstants.ENETUNREACH
 import android.system.OsConstants.ETH_P_IPV6
 import android.system.OsConstants.IPPROTO_IPV6
 import android.system.OsConstants.IPPROTO_UDP
 import android.system.OsConstants.RT_SCOPE_LINK
-import android.system.OsConstants.SOCK_DGRAM
 import android.util.Log
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry
@@ -98,12 +91,11 @@
 import com.android.testutils.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.ServiceUpdated
 import com.android.testutils.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.ServiceUpdatedLost
 import com.android.testutils.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.UnregisterCallbackSucceeded
+import com.android.testutils.PollPacketReader
 import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
 import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
-import com.android.testutils.PollPacketReader
 import com.android.testutils.TestDnsPacket
 import com.android.testutils.TestableNetworkAgent
-import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkCreated
 import com.android.testutils.TestableNetworkCallback
 import com.android.testutils.assertEmpty
 import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk30
@@ -244,16 +236,12 @@
         val tnm = context.getSystemService(TestNetworkManager::class.java)!!
         val iface = tnm.createTapInterface()
         val cb = TestableNetworkCallback()
-        val testNetworkSpecifier = TestNetworkSpecifier(iface.interfaceName)
         cm.requestNetwork(
-            NetworkRequest.Builder()
-                .removeCapability(NET_CAPABILITY_TRUSTED)
-                .addTransportType(TRANSPORT_TEST)
-                .setNetworkSpecifier(testNetworkSpecifier)
-                .build(),
+            TestableNetworkAgent.makeNetworkRequestForInterface(iface.interfaceName),
             cb
         )
-        val agent = registerTestNetworkAgent(iface.interfaceName)
+        val agent = TestableNetworkAgent.createOnInterface(context, handlerThread.looper,
+            iface.interfaceName, TIMEOUT_MS)
         val network = agent.network ?: fail("Registered agent should have a network")
 
         cb.eventuallyExpect<LinkPropertiesChanged>(TIMEOUT_MS) {
@@ -268,57 +256,6 @@
         return TestTapNetwork(iface, cb, agent, network)
     }
 
-    private fun registerTestNetworkAgent(ifaceName: String): TestableNetworkAgent {
-        val lp = LinkProperties().apply {
-            interfaceName = ifaceName
-        }
-        val agent = TestableNetworkAgent(
-            context,
-            handlerThread.looper,
-                NetworkCapabilities().apply {
-                    removeCapability(NET_CAPABILITY_TRUSTED)
-                    addTransportType(TRANSPORT_TEST)
-                    setNetworkSpecifier(TestNetworkSpecifier(ifaceName))
-                },
-            lp,
-            NetworkAgentConfig.Builder().build()
-        )
-        val network = agent.register()
-        agent.markConnected()
-        agent.expectCallback<OnNetworkCreated>()
-
-        // Wait until the link-local address can be used. Address flags are not available without
-        // elevated permissions, so check that bindSocket works.
-        PollingCheck.check("No usable v6 address on interface after $TIMEOUT_MS ms", TIMEOUT_MS) {
-            // To avoid race condition between socket connection succeeding and interface returning
-            // a non-empty address list. Verify that interface returns a non-empty list, before
-            // trying the socket connection.
-            if (NetworkInterface.getByName(ifaceName).interfaceAddresses.isEmpty()) {
-                return@check false
-            }
-
-            val sock = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP)
-            tryTest {
-                network.bindSocket(sock)
-                Os.connect(sock, parseNumericAddress("ff02::fb%$ifaceName"), 12345)
-                true
-            }.catch<ErrnoException> {
-                if (it.errno != ENETUNREACH && it.errno != EADDRNOTAVAIL) {
-                    throw it
-                }
-                false
-            } cleanup {
-                Os.close(sock)
-            }
-        }
-
-        lp.setLinkAddresses(NetworkInterface.getByName(ifaceName).interfaceAddresses.map {
-            LinkAddress(it.address, it.networkPrefixLength.toInt())
-        })
-        agent.sendLinkProperties(lp)
-        return agent
-    }
-
     private fun makeTestServiceInfo(network: Network? = null) = NsdServiceInfo().also {
         it.serviceType = serviceType
         it.serviceName = serviceName
@@ -573,7 +510,9 @@
             assertEquals(testNetwork1.network, serviceLost.serviceInfo.network)
 
             val newAgent = runAsShell(MANAGE_TEST_NETWORKS) {
-                registerTestNetworkAgent(testNetwork1.iface.interfaceName)
+                TestableNetworkAgent.createOnInterface(context, handlerThread.looper,
+                    testNetwork1.iface.interfaceName,
+                    TIMEOUT_MS)
             }
             val newNetwork = newAgent.network ?: fail("Registered agent should have a network")
             val serviceDiscovered3 = discoveryRecord.expectCallback<ServiceFound>()
@@ -2629,6 +2568,49 @@
         verifyCachedServicesRemoval(isCachedServiceRemoved = true)
     }
 
+    @Test
+    fun testSkipProbing() {
+        val si = makeTestServiceInfo(testNetwork1.network)
+        val request = AdvertisingRequest.Builder(si)
+            .setFlags(FLAG_SKIP_PROBING)
+            .build()
+        assertEquals(FLAG_SKIP_PROBING, request.flags)
+        assertEquals(PROTOCOL_DNS_SD, request.protocolType)
+        assertEquals(si.serviceName, request.serviceInfo.serviceName)
+
+        // Register service on testNetwork1
+        val registrationRecord = NsdRegistrationRecord()
+        nsdManager.registerService(request, { it.run() }, registrationRecord)
+        registrationRecord.expectCallback<ServiceRegistered>()
+        val packetReader = makePacketReader()
+
+        tryTest {
+            val srvRecordName = "$serviceName.$serviceType.local"
+            // Look for either announcements or probes
+            val packet = packetReader.pollForMdnsPacket {
+                it.isProbeFor(srvRecordName) || it.isReplyFor(srvRecordName)
+            }
+            assertNotNull(packet, "Probe or announcement not received within timeout")
+            // The first packet should be an announcement, not a probe.
+            assertTrue("Found initial probes with NSD_ADVERTISING_SKIP_PROBING enabled",
+                packet.isReplyFor(srvRecordName))
+
+            // Force a conflict now that the service is getting announced
+            val conflictingAnnouncement = buildConflictingAnnouncement()
+            packetReader.sendResponse(conflictingAnnouncement)
+
+            // Expect to see probes now (RFC6762 9., service is reset to probing state)
+            assertNotNull(packetReader.pollForProbe(serviceName, serviceType),
+                "Probe not received within timeout after conflict")
+        } cleanupStep {
+            nsdManager.unregisterService(registrationRecord)
+            registrationRecord.expectCallback<ServiceUnregistered>()
+        } cleanup {
+            packetReader.handler.post { packetReader.stop() }
+            handlerThread.waitForIdle(TIMEOUT_MS)
+        }
+    }
+
     private fun hasServiceTypeClientsForNetwork(clients: List<String>, network: Network): Boolean {
         return clients.any { client -> client.substring(
                 client.indexOf("network=") + "network=".length,
diff --git a/tests/cts/net/src/android/net/cts/TrafficStatsTest.java b/tests/cts/net/src/android/net/cts/TrafficStatsTest.java
index bd9e03c..f5198e3 100755
--- a/tests/cts/net/src/android/net/cts/TrafficStatsTest.java
+++ b/tests/cts/net/src/android/net/cts/TrafficStatsTest.java
@@ -24,6 +24,8 @@
 import android.util.Log;
 import android.util.Range;
 
+import com.android.testutils.ConnectivityDiagnosticsCollector;
+
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -94,11 +96,12 @@
 
     long tcpPacketToIpBytes(long packetCount, long bytes) {
         // ip header + tcp header + data.
-        // Tcp header is mostly 32. Syn has different tcp options -> 40. Don't care.
+        // Tcp header is mostly 32. Syn has different tcp options -> 40.
         return packetCount * (20 + 32 + bytes);
     }
 
     @AppModeFull(reason = "Socket cannot bind in instant app mode")
+    @ConnectivityDiagnosticsCollector.CollectTcpdumpOnFailure
     public void testTrafficStatsForLocalhost() throws IOException {
         final long mobileTxPacketsBefore = TrafficStats.getMobileTxPackets();
         final long mobileRxPacketsBefore = TrafficStats.getMobileRxPackets();
@@ -224,9 +227,15 @@
                 - uidTxDeltaPackets;
         final long deltaRxOtherPackets = (totalRxPacketsAfter - totalRxPacketsBefore)
                 - uidRxDeltaPackets;
-        if (deltaTxOtherPackets > 0 || deltaRxOtherPackets > 0) {
+        final long deltaTxOtherPktBytes = (totalTxBytesAfter - totalTxBytesBefore)
+                - uidTxDeltaBytes;
+        final long deltaRxOtherPktBytes  = (totalRxBytesAfter - totalRxBytesBefore)
+                - uidRxDeltaBytes;
+        if (deltaTxOtherPackets != 0 || deltaRxOtherPackets != 0
+                || deltaTxOtherPktBytes != 0 || deltaRxOtherPktBytes != 0) {
             Log.i(LOG_TAG, "lingering traffic data: " + deltaTxOtherPackets + "/"
-                    + deltaRxOtherPackets);
+                    + deltaRxOtherPackets + "/" + deltaTxOtherPktBytes
+                    + "/" + deltaRxOtherPktBytes);
         }
 
         // Check that the per-uid stats obtained from data profiling contain the expected values.
@@ -237,9 +246,9 @@
         final long pktBytes = tcpPacketToIpBytes(packetCount, byteCount);
         final long pktWithNoDataBytes = tcpPacketToIpBytes(packetCount, 0);
         final long minExpExtraPktBytes = tcpPacketToIpBytes(minExpectedExtraPackets, 0);
-        final long maxExpExtraPktBytes = tcpPacketToIpBytes(maxExpectedExtraPackets, 0);
-        final long deltaTxOtherPktBytes = tcpPacketToIpBytes(deltaTxOtherPackets, 0);
-        final long deltaRxOtherPktBytes  = tcpPacketToIpBytes(deltaRxOtherPackets, 0);
+        // Syn/syn-ack has different tcp options, make tcp header 40 for upper bound estimation.
+        final long maxExpExtraPktBytes = tcpPacketToIpBytes(maxExpectedExtraPackets, 8);
+
         assertInRange("txPackets detail", entry.txPackets, packetCount + minExpectedExtraPackets,
                 uidTxDeltaPackets);
         assertInRange("rxPackets detail", entry.rxPackets, packetCount + minExpectedExtraPackets,
@@ -257,32 +266,24 @@
         assertInRange("uidrxb", uidRxDeltaBytes, pktBytes + minExpExtraPktBytes,
                 pktBytes + pktWithNoDataBytes + maxExpExtraPktBytes + deltaRxOtherPktBytes);
         assertInRange("iftxp", ifaceTxDeltaPackets, packetCount + minExpectedExtraPackets,
-                packetCount + packetCount + maxExpectedExtraPackets);
+                packetCount + packetCount + maxExpectedExtraPackets + deltaTxOtherPackets);
         assertInRange("ifrxp", ifaceRxDeltaPackets, packetCount + minExpectedExtraPackets,
-                packetCount + packetCount + maxExpectedExtraPackets);
+                packetCount + packetCount + maxExpectedExtraPackets + deltaRxOtherPackets);
         assertInRange("iftxb", ifaceTxDeltaBytes, pktBytes + minExpExtraPktBytes,
-                pktBytes + pktWithNoDataBytes + maxExpExtraPktBytes);
+                pktBytes + pktWithNoDataBytes + maxExpExtraPktBytes + deltaTxOtherPktBytes);
         assertInRange("ifrxb", ifaceRxDeltaBytes, pktBytes + minExpExtraPktBytes,
-                pktBytes + pktWithNoDataBytes + maxExpExtraPktBytes);
+                pktBytes + pktWithNoDataBytes + maxExpExtraPktBytes + deltaRxOtherPktBytes);
 
         // Localhost traffic *does* count against total stats.
         // Check the total stats increased after test data transfer over localhost has been made.
-        assertTrue("ttxp: " + totalTxPacketsBefore + " -> " + totalTxPacketsAfter,
-                totalTxPacketsAfter >= totalTxPacketsBefore + uidTxDeltaPackets);
-        assertTrue("trxp: " + totalRxPacketsBefore + " -> " + totalRxPacketsAfter,
-                totalRxPacketsAfter >= totalRxPacketsBefore + uidRxDeltaPackets);
-        assertTrue("ttxb: " + totalTxBytesBefore + " -> " + totalTxBytesAfter,
-                totalTxBytesAfter >= totalTxBytesBefore + uidTxDeltaBytes);
-        assertTrue("trxb: " + totalRxBytesBefore + " -> " + totalRxBytesAfter,
-                totalRxBytesAfter >= totalRxBytesBefore + uidRxDeltaBytes);
-        assertTrue("iftxp: " + ifaceTxPacketsBefore + " -> " + ifaceTxPacketsAfter,
-                totalTxPacketsAfter >= totalTxPacketsBefore + ifaceTxDeltaPackets);
-        assertTrue("ifrxp: " + ifaceRxPacketsBefore + " -> " + ifaceRxPacketsAfter,
-                totalRxPacketsAfter >= totalRxPacketsBefore + ifaceRxDeltaPackets);
-        assertTrue("iftxb: " + ifaceTxBytesBefore + " -> " + ifaceTxBytesAfter,
-            totalTxBytesAfter >= totalTxBytesBefore + ifaceTxDeltaBytes);
-        assertTrue("ifrxb: " + ifaceRxBytesBefore + " -> " + ifaceRxBytesAfter,
-            totalRxBytesAfter >= totalRxBytesBefore + ifaceRxDeltaBytes);
+        assertInRange("ttxp", totalTxPacketsAfter,
+                totalTxPacketsBefore + packetCount + minExpectedExtraPackets, Long.MAX_VALUE);
+        assertInRange("trxp", totalRxPacketsAfter,
+                totalRxPacketsBefore + packetCount + minExpectedExtraPackets, Long.MAX_VALUE);
+        assertInRange("ttxb", totalTxBytesAfter,
+                totalTxBytesBefore + pktBytes + minExpExtraPktBytes, Long.MAX_VALUE);
+        assertInRange("trxb", totalRxBytesAfter,
+                totalRxBytesBefore + pktBytes + minExpExtraPktBytes, Long.MAX_VALUE);
 
         // Localhost traffic should *not* count against mobile stats,
         // There might be some other traffic, but nowhere near 1MB.
diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java
index 243cd27..75b2814 100644
--- a/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java
+++ b/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java
@@ -22,6 +22,7 @@
 import static android.Manifest.permission.TETHER_PRIVILEGED;
 import static android.net.TetheringManager.TETHERING_WIFI;
 import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_REQUEST;
 import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_FAILED;
 import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STARTED;
 import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STOPPED;
@@ -126,6 +127,67 @@
         }
     }
 
+    public static class StopTetheringCallback implements TetheringManager.StopTetheringCallback {
+        private static final int TIMEOUT_MS = 30_000;
+        public static class CallbackValue {
+            public final int error;
+
+            private CallbackValue(final int e) {
+                error = e;
+            }
+
+            public static class OnStopTetheringSucceeded extends CallbackValue {
+                OnStopTetheringSucceeded() {
+                    super(TETHER_ERROR_NO_ERROR);
+                }
+            }
+
+            public static class OnStopTetheringFailed extends CallbackValue {
+                OnStopTetheringFailed(final int error) {
+                    super(error);
+                }
+            }
+
+            @Override
+            public String toString() {
+                return String.format("%s(%d)", getClass().getSimpleName(), error);
+            }
+        }
+
+        private final ArrayTrackRecord<CallbackValue>.ReadHead mHistory =
+                new ArrayTrackRecord<CallbackValue>().newReadHead();
+
+        @Override
+        public void onStopTetheringSucceeded() {
+            mHistory.add(new CallbackValue.OnStopTetheringSucceeded());
+        }
+
+        @Override
+        public void onStopTetheringFailed(final int error) {
+            mHistory.add(new CallbackValue.OnStopTetheringFailed(error));
+        }
+
+        /**
+         *  Verifies that {@link #onStopTetheringSucceeded()} was called
+         */
+        public void verifyStopTetheringSucceeded() {
+            final CallbackValue cv = mHistory.poll(TIMEOUT_MS, c -> true);
+            assertNotNull("No onStopTetheringSucceeded after " + TIMEOUT_MS + " ms", cv);
+            assertTrue("Fail stop tethering:" + cv,
+                    cv instanceof CallbackValue.OnStopTetheringSucceeded);
+        }
+
+        /**
+         *  Verifies that {@link #onStopTetheringFailed(int)} was called
+         */
+        public void expectStopTetheringFailed(final int expected) {
+            final CallbackValue cv = mHistory.poll(TIMEOUT_MS, c -> true);
+            assertNotNull("No onStopTetheringFailed after " + TIMEOUT_MS + " ms", cv);
+            assertTrue("Expect fail with error code " + expected + ", but received: " + cv,
+                    (cv instanceof CallbackValue.OnStopTetheringFailed) && (cv.error == expected));
+        }
+    }
+
     private static boolean isRegexMatch(final String[] ifaceRegexs, String iface) {
         if (ifaceRegexs == null) fail("ifaceRegexs should not be null");
 
@@ -574,6 +636,22 @@
         expectSoftApDisabled();
     }
 
+    /**
+     * Calls {@link TetheringManager#stopTethering(TetheringRequest, Executor,
+     * TetheringManager.StopTetheringCallback)} and verifies if it succeeded or failed.
+     */
+    public void stopTethering(final TetheringRequest request, boolean expectSuccess) {
+        final StopTetheringCallback stopTetheringCallback = new StopTetheringCallback();
+        runAsShell(TETHER_PRIVILEGED, () -> {
+            mTm.stopTethering(request, c -> c.run() /* executor */, stopTetheringCallback);
+            if (expectSuccess) {
+                stopTetheringCallback.verifyStopTetheringSucceeded();
+            } else {
+                stopTetheringCallback.expectStopTetheringFailed(TETHER_ERROR_UNKNOWN_REQUEST);
+            }
+        });
+    }
+
     public void stopAllTethering() {
         final TestTetheringEventCallback callback = registerTetheringEventCallback();
         try {
diff --git a/tests/cts/netpermission/updatestatspermission/Android.bp b/tests/cts/netpermission/updatestatspermission/Android.bp
index b324dc8..0ff98e7 100644
--- a/tests/cts/netpermission/updatestatspermission/Android.bp
+++ b/tests/cts/netpermission/updatestatspermission/Android.bp
@@ -33,6 +33,7 @@
 
     // Tag this module as a cts test artifact
     test_suites: [
+        "automotive-general-tests",
         "cts",
         "general-tests",
     ],
diff --git a/tests/cts/netpermission/updatestatspermission/AndroidTest.xml b/tests/cts/netpermission/updatestatspermission/AndroidTest.xml
index fb6c814..82994c4 100644
--- a/tests/cts/netpermission/updatestatspermission/AndroidTest.xml
+++ b/tests/cts/netpermission/updatestatspermission/AndroidTest.xml
@@ -20,6 +20,7 @@
     <option name="config-descriptor:metadata" key="parameter" value="instant_app" />
     <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
     <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+    <option name="config-descriptor:metadata" key="parameter" value="secondary_user_on_secondary_display" />
     <option name="not-shardable" value="true" />
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true" />
diff --git a/tests/cts/tethering/Android.bp b/tests/cts/tethering/Android.bp
index d9bc7f7..a1e0797 100644
--- a/tests/cts/tethering/Android.bp
+++ b/tests/cts/tethering/Android.bp
@@ -34,6 +34,7 @@
 
     static_libs: [
         "TetheringCommonTests",
+        "com.android.net.flags-aconfig-java",
         "compatibility-device-util-axt",
         "cts-net-utils",
         "net-tests-utils",
@@ -54,34 +55,6 @@
     host_required: ["net-tests-utils-host-common"],
 }
 
-// Tethering CTS tests that target the latest released SDK. These tests can be installed on release
-// devices which has equal or lowner sdk version than target sdk and are useful for qualifying
-// mainline modules on release devices.
-android_test {
-    name: "CtsTetheringTestLatestSdk",
-    defaults: [
-        "ConnectivityTestsLatestSdkDefaults",
-        "CtsTetheringTestDefaults",
-    ],
-
-    min_sdk_version: "30",
-
-    static_libs: [
-        "TetheringIntegrationTestsLatestSdkLib",
-    ],
-
-    test_suites: [
-        "general-tests",
-        "mts-tethering",
-    ],
-
-    test_config_template: "AndroidTestTemplate.xml",
-
-    // Include both the 32 and 64 bit versions
-    compile_multilib: "both",
-    jarjar_rules: ":NetworkStackJarJarRules",
-}
-
 // Tethering CTS tests for development and release. These tests always target the platform SDK
 // version, and are subject to all the restrictions appropriate to that version. Before SDK
 // finalization, these tests have a min_sdk_version of 10000, but they can still be installed on
diff --git a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
index b294d63..9e49926 100644
--- a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
+++ b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
@@ -17,6 +17,7 @@
 
 import static android.Manifest.permission.MODIFY_PHONE_STATE;
 import static android.Manifest.permission.TETHER_PRIVILEGED;
+import static android.Manifest.permission.WRITE_SETTINGS;
 import static android.content.pm.PackageManager.FEATURE_TELEPHONY;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
@@ -28,6 +29,7 @@
 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.TETHER_ERROR_ENTITLEMENT_UNKNOWN;
@@ -43,6 +45,7 @@
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeFalse;
@@ -70,6 +73,7 @@
 import android.net.wifi.SoftApConfiguration;
 import android.net.wifi.WifiManager;
 import android.net.wifi.WifiSsid;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.PersistableBundle;
 import android.os.ResultReceiver;
@@ -82,10 +86,13 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.modules.utils.build.SdkLevel;
+import com.android.net.flags.Flags;
 import com.android.testutils.ParcelUtils;
+import com.android.testutils.com.android.testutils.CarrierConfigRule;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -100,6 +107,8 @@
 
 @RunWith(AndroidJUnit4.class)
 public class TetheringManagerTest {
+    @Rule
+    public final CarrierConfigRule mCarrierConfigRule = new CarrierConfigRule();
 
     private Context mContext;
 
@@ -303,6 +312,13 @@
         tr3.setUid(uid);
         tr3.setPackageName(packageName);
         assertEquals(tr2, tr3);
+
+        final String interfaceName = "test_iface";
+        final TetheringRequest tr4 = new TetheringRequest.Builder(TETHERING_VIRTUAL)
+                .setInterfaceName(interfaceName)
+                .setConnectivityScope(CONNECTIVITY_SCOPE_GLOBAL).build();
+        assertEquals(interfaceName, tr4.getInterfaceName());
+        assertEquals(CONNECTIVITY_SCOPE_GLOBAL, tr4.getConnectivityScope());
     }
 
     @Test
@@ -320,23 +336,40 @@
     }
 
     @Test
+    public void testTetheringRequestSetInterfaceNameFailsExceptTetheringVirtual() {
+        for (int type : List.of(TETHERING_USB, TETHERING_BLUETOOTH, TETHERING_NCM,
+                TETHERING_ETHERNET)) {
+            try {
+                new TetheringRequest.Builder(type).setInterfaceName("test_iface");
+                fail("Was able to set interface name for tethering type " + type);
+            } catch (IllegalArgumentException e) {
+                // Success
+            }
+        }
+    }
+
+    @Test
     public void testTetheringRequestParcelable() {
         final SoftApConfiguration softApConfiguration = createSoftApConfiguration("SSID");
         final LinkAddress localAddr = new LinkAddress("192.168.24.5/24");
         final LinkAddress clientAddr = new LinkAddress("192.168.24.100/24");
-        final TetheringRequest withConfig = new TetheringRequest.Builder(TETHERING_WIFI)
+        final TetheringRequest withApConfig = new TetheringRequest.Builder(TETHERING_WIFI)
                 .setSoftApConfiguration(softApConfiguration)
                 .setStaticIpv4Addresses(localAddr, clientAddr)
                 .setExemptFromEntitlementCheck(true)
                 .setShouldShowEntitlementUi(false).build();
-        final TetheringRequest withoutConfig = new TetheringRequest.Builder(TETHERING_WIFI)
+        final TetheringRequest withoutApConfig = new TetheringRequest.Builder(TETHERING_WIFI)
                 .setStaticIpv4Addresses(localAddr, clientAddr)
                 .setExemptFromEntitlementCheck(true)
                 .setShouldShowEntitlementUi(false).build();
-        assertEquals(withConfig, ParcelUtils.parcelingRoundTrip(withConfig));
-        assertEquals(withoutConfig, ParcelUtils.parcelingRoundTrip(withoutConfig));
-        assertNotEquals(withConfig, ParcelUtils.parcelingRoundTrip(withoutConfig));
-        assertNotEquals(withoutConfig, ParcelUtils.parcelingRoundTrip(withConfig));
+        assertEquals(withApConfig, ParcelUtils.parcelingRoundTrip(withApConfig));
+        assertEquals(withoutApConfig, ParcelUtils.parcelingRoundTrip(withoutApConfig));
+        assertNotEquals(withApConfig, ParcelUtils.parcelingRoundTrip(withoutApConfig));
+        assertNotEquals(withoutApConfig, ParcelUtils.parcelingRoundTrip(withApConfig));
+
+        final TetheringRequest withIfaceName = new TetheringRequest.Builder(TETHERING_VIRTUAL)
+                .setInterfaceName("test_iface").build();
+        assertEquals(withIfaceName, ParcelUtils.parcelingRoundTrip(withIfaceName));
     }
 
     @Test
@@ -358,18 +391,21 @@
 
             mCtsTetheringUtils.stopWifiTethering(tetherEventCallback);
 
-            try {
-                final int ret = runAsShell(TETHER_PRIVILEGED, () -> mTM.tether(wifiTetheringIface));
-                // There is no guarantee that the wifi interface will be available after disabling
-                // the hotspot, so don't fail the test if the call to tether() fails.
-                if (ret == TETHER_ERROR_NO_ERROR) {
-                    // If calling #tether successful, there is a callback to tell the result of
-                    // tethering setup.
-                    tetherEventCallback.expectErrorOrTethered(
-                            new TetheringInterface(TETHERING_WIFI, wifiTetheringIface));
+            if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+                try {
+                    final int ret = runAsShell(TETHER_PRIVILEGED,
+                            () -> mTM.tether(wifiTetheringIface));
+                    // There is no guarantee that the wifi interface will be available after
+                    // disabling the hotspot, so don't fail the test if the call to tether() fails.
+                    if (ret == TETHER_ERROR_NO_ERROR) {
+                        // If calling #tether successful, there is a callback to tell the result of
+                        // tethering setup.
+                        tetherEventCallback.expectErrorOrTethered(
+                                new TetheringInterface(TETHERING_WIFI, wifiTetheringIface));
+                    }
+                } finally {
+                    runAsShell(TETHER_PRIVILEGED, () -> mTM.untether(wifiTetheringIface));
                 }
-            } finally {
-                runAsShell(TETHER_PRIVILEGED, () -> mTM.untether(wifiTetheringIface));
             }
         } finally {
             mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
@@ -420,11 +456,52 @@
     }
 
     @Test
-    public void testEnableTetheringPermission() throws Exception {
+    public void testStopTetheringRequest() throws Exception {
+        assumeTrue(isTetheringWithSoftApConfigEnabled());
+        final TestTetheringEventCallback tetherEventCallback =
+                mCtsTetheringUtils.registerTetheringEventCallback();
+        try {
+            tetherEventCallback.assumeWifiTetheringSupported(mContext);
+
+            // stopTethering without any tethering active should fail.
+            TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build();
+            mCtsTetheringUtils.stopTethering(request, false /* expectSuccess */);
+
+            // Start wifi tethering
+            mCtsTetheringUtils.startWifiTethering(tetherEventCallback);
+
+            // stopTethering should succeed now that there's a request.
+            mCtsTetheringUtils.stopTethering(request, true /* expectSuccess */);
+            tetherEventCallback.expectNoTetheringActive();
+        } finally {
+            mCtsTetheringUtils.stopAllTethering();
+            mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
+        }
+    }
+
+    private boolean isTetheringWithSoftApConfigEnabled() {
+        return Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM
+                && Flags.tetheringWithSoftApConfig();
+    }
+
+    @Test
+    public void testStartTetheringNoPermission() throws Exception {
         final StartTetheringCallback startTetheringCallback = new StartTetheringCallback();
+
+        // No permission
         mTM.startTethering(new TetheringRequest.Builder(TETHERING_WIFI).build(),
                 c -> c.run() /* executor */, startTetheringCallback);
         startTetheringCallback.expectTetheringFailed(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+
+        // WRITE_SETTINGS not sufficient
+        if (isTetheringWithSoftApConfigEnabled()) {
+            runAsShell(WRITE_SETTINGS, () -> {
+                mTM.startTethering(new TetheringRequest.Builder(TETHERING_WIFI).build(),
+                        c -> c.run() /* executor */, startTetheringCallback);
+                startTetheringCallback.expectTetheringFailed(
+                        TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+            });
+        }
     }
 
     private class EntitlementResultListener implements OnTetheringEntitlementResultListener {
@@ -489,22 +566,13 @@
         // Override carrier config to ignore entitlement check.
         final PersistableBundle bundle = new PersistableBundle();
         bundle.putBoolean(CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL, false);
-        overrideCarrierConfig(bundle);
+        mCarrierConfigRule.addConfigOverrides(
+                SubscriptionManager.getDefaultSubscriptionId(), bundle);
 
         // Verify that requestLatestTetheringEntitlementResult() can get entitlement
         // result TETHER_ERROR_NO_ERROR due to provisioning bypassed.
         assertEntitlementResult(listener -> mTM.requestLatestTetheringEntitlementResult(
                 TETHERING_WIFI, false, c -> c.run(), listener), TETHER_ERROR_NO_ERROR);
-
-        // Reset carrier config.
-        overrideCarrierConfig(null);
-    }
-
-    private void overrideCarrierConfig(PersistableBundle bundle) {
-        final CarrierConfigManager configManager = (CarrierConfigManager) mContext
-                .getSystemService(Context.CARRIER_CONFIG_SERVICE);
-        final int subId = SubscriptionManager.getDefaultSubscriptionId();
-        runAsShell(MODIFY_PHONE_STATE, () -> configManager.overrideConfig(subId, bundle));
     }
 
     private boolean isTetheringApnRequired() {
@@ -565,4 +633,11 @@
             }
         }
     }
+
+    @Test
+    public void testLegacyTetherApisThrowUnsupportedOperationExceptionAfterV() {
+        assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM);
+        assertThrows(UnsupportedOperationException.class, () -> mTM.tether("iface"));
+        assertThrows(UnsupportedOperationException.class, () -> mTM.untether("iface"));
+    }
 }
diff --git a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
index 06bdca6..437eb81 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.server.net.integrationtests
 
+import android.Manifest.permission
 import android.app.usage.NetworkStatsManager
 import android.content.ComponentName
 import android.content.Context
@@ -54,18 +55,21 @@
 import com.android.networkstack.apishim.TelephonyManagerShimImpl
 import com.android.server.BpfNetMaps
 import com.android.server.ConnectivityService
+import com.android.server.L2capNetworkProvider
 import com.android.server.NetworkAgentWrapper
 import com.android.server.TestNetIdManager
 import com.android.server.connectivity.CarrierPrivilegeAuthenticator
 import com.android.server.connectivity.ConnectivityResources
 import com.android.server.connectivity.MockableSystemProperties
 import com.android.server.connectivity.MultinetworkPolicyTracker
+import com.android.server.connectivity.PermissionMonitor
 import com.android.server.connectivity.ProxyTracker
 import com.android.server.connectivity.SatelliteAccessController
 import com.android.testutils.DevSdkIgnoreRunner
 import com.android.testutils.DeviceInfoUtils
 import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
 import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.runAsShell
 import com.android.testutils.tryTest
 import java.util.function.BiConsumer
 import java.util.function.Consumer
@@ -208,7 +212,9 @@
         networkStackClient = TestNetworkStackClient(realContext)
         networkStackClient.start()
 
-        service = TestConnectivityService(TestDependencies())
+        service = runAsShell(permission.OBSERVE_GRANT_REVOKE_PERMISSIONS) {
+            TestConnectivityService(TestDependencies())
+        }
         cm = ConnectivityManager(context, service)
         context.addMockSystemService(Context.CONNECTIVITY_SERVICE, cm)
         context.addMockSystemService(Context.NETWORK_STATS_SERVICE, statsManager)
@@ -217,7 +223,7 @@
     }
 
     private inner class TestConnectivityService(deps: Dependencies) : ConnectivityService(
-            context, dnsResolver, log, netd, deps)
+            context, dnsResolver, log, netd, deps, PermissionMonitorDependencies())
 
     private inner class TestDependencies : ConnectivityService.Dependencies() {
         override fun getNetworkStack() = networkStackClient
@@ -268,6 +274,12 @@
             connectivityServiceInternalHandler: Handler
         ): SatelliteAccessController? = mock(
             SatelliteAccessController::class.java)
+
+        override fun makeL2capNetworkProvider(context: Context) = null
+    }
+
+    private inner class PermissionMonitorDependencies : PermissionMonitor.Dependencies() {
+        override fun shouldEnforceLocalNetRestrictions(uid: Int) = false
     }
 
     @After
diff --git a/tests/native/connectivity_native_test/Android.bp b/tests/native/connectivity_native_test/Android.bp
index 39a08fa..02ac3c5 100644
--- a/tests/native/connectivity_native_test/Android.bp
+++ b/tests/native/connectivity_native_test/Android.bp
@@ -24,7 +24,6 @@
         "libcom.android.tethering.connectivity_native",
         "liblog",
         "libnetutils",
-        "libprocessgroup",
     ],
     static_libs: [
         "connectivity_native_aidl_interface-lateststable-ndk",
diff --git a/tests/unit/java/android/net/TrafficStatsTest.kt b/tests/unit/java/android/net/TrafficStatsTest.kt
new file mode 100644
index 0000000..0f85daf
--- /dev/null
+++ b/tests/unit/java/android/net/TrafficStatsTest.kt
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net
+
+import android.net.TrafficStats.UNSUPPORTED
+import android.net.netstats.StatsResult
+import android.net.netstats.TrafficStatsRateLimitCacheConfig
+import android.os.Build
+import com.android.server.net.NetworkStatsService.TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG
+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 org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import java.util.HashMap
+import java.util.function.LongSupplier
+
+const val TEST_EXPIRY_DURATION_MS = 1000
+const val TEST_IFACE = "wlan0"
+
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+class TrafficStatsTest {
+    private val binder = mock(INetworkStatsService::class.java)
+    private val myUid = android.os.Process.myUid()
+    private val mockMyUidStatsResult = StatsResult(5L, 6L, 7L, 8L)
+    private val mockIfaceStatsResult = StatsResult(7L, 3L, 10L, 21L)
+    private val mockTotalStatsResult = StatsResult(8L, 1L, 5L, 2L)
+    private val secondUidStatsResult = StatsResult(3L, 7L, 10L, 5L)
+    private val secondIfaceStatsResult = StatsResult(9L, 8L, 7L, 6L)
+    private val secondTotalStatsResult = StatsResult(4L, 3L, 2L, 1L)
+    private val emptyStatsResult = StatsResult(0L, 0L, 0L, 0L)
+    private val unsupportedStatsResult =
+            StatsResult(UNSUPPORTED.toLong(), UNSUPPORTED.toLong(),
+                    UNSUPPORTED.toLong(), UNSUPPORTED.toLong())
+
+    private val cacheDisabledConfig = TrafficStatsRateLimitCacheConfig.Builder()
+            .setIsCacheEnabled(false)
+            .setExpiryDurationMs(0)
+            .setMaxEntries(0)
+            .build()
+    private val cacheEnabledConfig = TrafficStatsRateLimitCacheConfig.Builder()
+            .setIsCacheEnabled(true)
+            .setExpiryDurationMs(TEST_EXPIRY_DURATION_MS)
+            .setMaxEntries(100)
+            .build()
+    private val mTestTimeSupplier = TestTimeSupplier()
+
+    private val featureFlags = HashMap<String, Boolean>()
+
+    // This will set feature flags from @FeatureFlag annotations
+    // into the map before setUp() runs.
+    @get:Rule
+    val setFeatureFlagsRule = SetFeatureFlagsRule(
+            { name, enabled -> featureFlags.put(name, enabled == true) },
+            { name -> featureFlags.getOrDefault(name, false) }
+    )
+
+    class TestTimeSupplier : LongSupplier {
+        private var currentTimeMillis = 0L
+
+        override fun getAsLong() = currentTimeMillis
+
+        fun advanceTime(millis: Int) {
+            currentTimeMillis += millis
+        }
+    }
+
+    @Before
+    fun setUp() {
+        TrafficStats.setServiceForTest(binder)
+        TrafficStats.setTimeSupplierForTest(mTestTimeSupplier)
+        mockStats(mockMyUidStatsResult, mockIfaceStatsResult, mockTotalStatsResult)
+        if (featureFlags.getOrDefault(TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG, false)) {
+            doReturn(cacheEnabledConfig).`when`(binder).getRateLimitCacheConfig()
+        } else {
+            doReturn(cacheDisabledConfig).`when`(binder).getRateLimitCacheConfig()
+        }
+        TrafficStats.reinitRateLimitCacheForTest()
+    }
+
+    @After
+    fun tearDown() {
+        TrafficStats.setServiceForTest(null)
+        TrafficStats.setTimeSupplierForTest(null)
+        TrafficStats.reinitRateLimitCacheForTest()
+    }
+
+    private fun assertUidStats(uid: Int, stats: StatsResult) {
+        assertEquals(stats.rxBytes, TrafficStats.getUidRxBytes(uid))
+        assertEquals(stats.rxPackets, TrafficStats.getUidRxPackets(uid))
+        assertEquals(stats.txBytes, TrafficStats.getUidTxBytes(uid))
+        assertEquals(stats.txPackets, TrafficStats.getUidTxPackets(uid))
+    }
+
+    private fun assertIfaceStats(iface: String, stats: StatsResult) {
+        assertEquals(stats.rxBytes, TrafficStats.getRxBytes(iface))
+        assertEquals(stats.rxPackets, TrafficStats.getRxPackets(iface))
+        assertEquals(stats.txBytes, TrafficStats.getTxBytes(iface))
+        assertEquals(stats.txPackets, TrafficStats.getTxPackets(iface))
+    }
+
+    private fun assertTotalStats(stats: StatsResult) {
+        assertEquals(stats.rxBytes, TrafficStats.getTotalRxBytes())
+        assertEquals(stats.rxPackets, TrafficStats.getTotalRxPackets())
+        assertEquals(stats.txBytes, TrafficStats.getTotalTxBytes())
+        assertEquals(stats.txPackets, TrafficStats.getTotalTxPackets())
+    }
+
+    private fun mockStats(uidStats: StatsResult?, ifaceStats: StatsResult?,
+                          totalStats: StatsResult?) {
+        doReturn(uidStats).`when`(binder).getUidStats(myUid)
+        doReturn(ifaceStats).`when`(binder).getIfaceStats(TEST_IFACE)
+        doReturn(totalStats).`when`(binder).getTotalStats()
+    }
+
+    private fun assertStats(uidStats: StatsResult, ifaceStats: StatsResult,
+                            totalStats: StatsResult) {
+        assertUidStats(myUid, uidStats)
+        assertIfaceStats(TEST_IFACE, ifaceStats)
+        assertTotalStats(totalStats)
+    }
+
+    private fun assertStatsFetchInvocations(wantedInvocations: Int) {
+        verify(binder, times(wantedInvocations)).getUidStats(myUid)
+        verify(binder, times(wantedInvocations)).getIfaceStats(TEST_IFACE)
+        verify(binder, times(wantedInvocations)).getTotalStats()
+    }
+
+    @FeatureFlag(name = TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG)
+    @Test
+    fun testRateLimitCacheExpiry_cacheEnabled() {
+        // Initial fetch, verify binder calls.
+        assertStats(mockMyUidStatsResult, mockIfaceStatsResult, mockTotalStatsResult)
+        assertStatsFetchInvocations(1)
+
+        // Advance time within expiry, verify cached values used.
+        clearInvocations(binder)
+        mockStats(secondUidStatsResult, secondIfaceStatsResult, secondTotalStatsResult)
+        mTestTimeSupplier.advanceTime(1)
+        assertStats(mockMyUidStatsResult, mockIfaceStatsResult, mockTotalStatsResult)
+        assertStatsFetchInvocations(0)
+
+        // Advance time to expire cache, verify new values fetched.
+        clearInvocations(binder)
+        mTestTimeSupplier.advanceTime(TEST_EXPIRY_DURATION_MS)
+        assertStats(secondUidStatsResult, secondIfaceStatsResult, secondTotalStatsResult)
+        assertStatsFetchInvocations(1)
+    }
+
+    @FeatureFlag(name = TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
+    @Test
+    fun testRateLimitCacheExpiry_cacheDisabled() {
+        // Initial fetch, verify binder calls.
+        assertStats(mockMyUidStatsResult, mockIfaceStatsResult, mockTotalStatsResult)
+        assertStatsFetchInvocations(4)
+
+        // Advance time within expiry, verify new values fetched.
+        clearInvocations(binder)
+        mockStats(secondUidStatsResult, secondIfaceStatsResult, secondTotalStatsResult)
+        mTestTimeSupplier.advanceTime(1)
+        assertStats(secondUidStatsResult, secondIfaceStatsResult, secondTotalStatsResult)
+        assertStatsFetchInvocations(4)
+    }
+
+    @FeatureFlag(name = TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG)
+    @Test
+    fun testInvalidStatsNotCached_cacheEnabled() {
+        doTestInvalidStatsNotCached()
+    }
+
+    @FeatureFlag(name = TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
+    @Test
+    fun testInvalidStatsNotCached_cacheDisabled() {
+        doTestInvalidStatsNotCached()
+    }
+
+    private fun doTestInvalidStatsNotCached() {
+        // Mock null stats, this usually happens when the query is not valid,
+        // e.g. query uid stats of other application.
+        mockStats(null, null, null)
+        assertStats(unsupportedStatsResult, unsupportedStatsResult, unsupportedStatsResult)
+        assertStatsFetchInvocations(4)
+
+        // Verify null stats is not cached, and mock empty stats. This usually
+        // happens when queries with non-existent interface names.
+        clearInvocations(binder)
+        mockStats(emptyStatsResult, emptyStatsResult, emptyStatsResult)
+        assertStats(emptyStatsResult, emptyStatsResult, emptyStatsResult)
+        assertStatsFetchInvocations(4)
+
+        // Verify empty result is also not cached.
+        clearInvocations(binder)
+        assertStats(emptyStatsResult, emptyStatsResult, emptyStatsResult)
+        assertStatsFetchInvocations(4)
+    }
+
+    @FeatureFlag(name = TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG)
+    @Test
+    fun testClearRateLimitCaches_cacheEnabled() {
+        doTestClearRateLimitCaches(true)
+    }
+
+    @FeatureFlag(name = TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
+    @Test
+    fun testClearRateLimitCaches_cacheDisabled() {
+        doTestClearRateLimitCaches(false)
+    }
+
+    private fun doTestClearRateLimitCaches(cacheEnabled: Boolean) {
+        // Initial fetch, verify binder calls.
+        assertStats(mockMyUidStatsResult, mockIfaceStatsResult, mockTotalStatsResult)
+        assertStatsFetchInvocations(if (cacheEnabled) 1 else 4)
+
+        // Verify cached values are used.
+        clearInvocations(binder)
+        assertStats(mockMyUidStatsResult, mockIfaceStatsResult, mockTotalStatsResult)
+        assertStatsFetchInvocations(if (cacheEnabled) 0 else 4)
+
+        // Clear caches, verify fetching from the service.
+        clearInvocations(binder)
+        TrafficStats.clearRateLimitCaches()
+        mockStats(secondUidStatsResult, secondIfaceStatsResult, secondTotalStatsResult)
+        assertStats(secondUidStatsResult, secondIfaceStatsResult, secondTotalStatsResult)
+        assertStatsFetchInvocations(if (cacheEnabled) 1 else 4)
+    }
+}
diff --git a/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt b/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt
index c491f37..8117431 100644
--- a/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt
+++ b/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt
@@ -44,14 +44,14 @@
             serviceType = "_ipp._tcp"
         }
         val beforeParcel = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
-                .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+                .setFlags(NSD_ADVERTISING_UPDATE_ONLY)
                 .setTtl(Duration.ofSeconds(30L))
                 .build()
 
         val afterParcel = parcelingRoundTrip(beforeParcel)
 
         assertEquals(beforeParcel.serviceInfo.serviceType, afterParcel.serviceInfo.serviceType)
-        assertEquals(beforeParcel.advertisingConfig, afterParcel.advertisingConfig)
+        assertEquals(beforeParcel.flags, afterParcel.flags)
     }
 
     @Test
@@ -72,13 +72,13 @@
             serviceType = "_ipp._tcp"
         }
         val request = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
-                .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+                .setFlags(NSD_ADVERTISING_UPDATE_ONLY)
                 .setTtl(Duration.ofSeconds(100L))
                 .build()
 
         assertEquals("_ipp._tcp", request.serviceInfo.serviceType)
         assertEquals(PROTOCOL_DNS_SD, request.protocolType)
-        assertEquals(NSD_ADVERTISING_UPDATE_ONLY, request.advertisingConfig)
+        assertEquals(NSD_ADVERTISING_UPDATE_ONLY, request.flags)
         assertEquals(Duration.ofSeconds(100L), request.ttl)
     }
 
@@ -90,11 +90,11 @@
         val request1 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD).build()
         val request2 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD).build()
         val request3 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
-                .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+                .setFlags(NSD_ADVERTISING_UPDATE_ONLY)
                 .setTtl(Duration.ofSeconds(120L))
                 .build()
         val request4 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
-                .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+                .setFlags(NSD_ADVERTISING_UPDATE_ONLY)
                 .setTtl(Duration.ofSeconds(120L))
                 .build()
 
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index c1c15ca..caf1765 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -71,6 +71,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
@@ -99,6 +100,7 @@
 
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.Struct.Bool;
 import com.android.net.module.util.Struct.S32;
 import com.android.net.module.util.Struct.U32;
 import com.android.net.module.util.Struct.U8;
@@ -106,6 +108,7 @@
 import com.android.net.module.util.bpf.CookieTagMapValue;
 import com.android.net.module.util.bpf.IngressDiscardKey;
 import com.android.net.module.util.bpf.IngressDiscardValue;
+import com.android.net.module.util.bpf.LocalNetAccessKey;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
@@ -171,6 +174,10 @@
     private final IBpfMap<S32, UidOwnerValue> mUidOwnerMap =
             new TestBpfMap<>(S32.class, UidOwnerValue.class);
     private final IBpfMap<S32, U8> mUidPermissionMap = new TestBpfMap<>(S32.class, U8.class);
+    private final IBpfMap<U32, Bool> mLocalNetBlockedUidMap =
+            new TestBpfMap<>(U32.class, Bool.class);
+    private final IBpfMap<LocalNetAccessKey, Bool> mLocalNetAccessMap =
+            new TestBpfMap<>(LocalNetAccessKey.class, Bool.class);
     private final IBpfMap<CookieTagMapKey, CookieTagMapValue> mCookieTagMap =
             spy(new TestBpfMap<>(CookieTagMapKey.class, CookieTagMapValue.class));
     private final IBpfMap<S32, U8> mDataSaverEnabledMap = new TestBpfMap<>(S32.class, U8.class);
@@ -189,6 +196,8 @@
                 CURRENT_STATS_MAP_CONFIGURATION_KEY, new U32(STATS_SELECT_MAP_A));
         BpfNetMaps.setUidOwnerMapForTest(mUidOwnerMap);
         BpfNetMaps.setUidPermissionMapForTest(mUidPermissionMap);
+        BpfNetMaps.setLocalNetAccessMapForTest(mLocalNetAccessMap);
+        BpfNetMaps.setLocalNetBlockedUidMapForTest(mLocalNetBlockedUidMap);
         BpfNetMaps.setCookieTagMapForTest(mCookieTagMap);
         BpfNetMaps.setDataSaverEnabledMapForTest(mDataSaverEnabledMap);
         mDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, new U8(DATA_SAVER_DISABLED));
@@ -235,6 +244,225 @@
     }
 
     @Test
+    @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testAddLocalNetAccessBeforeV() {
+        assertThrows(UnsupportedOperationException.class, () ->
+                mBpfNetMaps.addLocalNetAccess(0, TEST_IF_NAME, Inet6Address.ANY, 0, 0, true));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testAddLocalNetAccessAfterV() throws Exception {
+        assertTrue(mLocalNetAccessMap.isEmpty());
+
+        mBpfNetMaps.addLocalNetAccess(160, TEST_IF_NAME,
+                Inet4Address.getByName("196.68.0.0"), 0, 0, true);
+
+        assertNotNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+                Inet4Address.getByName("196.68.0.0"), 0, 0)));
+        assertNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+                Inet4Address.getByName("100.68.0.0"), 0, 0)));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testAddLocalNetAccessWithNullInterfaceAfterV() throws Exception {
+        assertTrue(mLocalNetAccessMap.isEmpty());
+
+        mBpfNetMaps.addLocalNetAccess(160, null,
+                Inet4Address.getByName("196.68.0.0"), 0, 0, true);
+
+        // As we tried to add null interface, it would be skipped and map should be empty.
+        assertTrue(mLocalNetAccessMap.isEmpty());
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testAddLocalNetAccessAfterVWithIncorrectInterface() throws Exception {
+        assertTrue(mLocalNetAccessMap.isEmpty());
+
+        // wlan2 is an incorrect interface
+        mBpfNetMaps.addLocalNetAccess(160, "wlan2",
+                Inet4Address.getByName("196.68.0.0"), 0, 0, true);
+
+        // As we tried to add incorrect interface, it would be skipped and map should be empty.
+        assertTrue(mLocalNetAccessMap.isEmpty());
+    }
+
+    @Test
+    @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testGetLocalNetAccessBeforeV() {
+        assertThrows(UnsupportedOperationException.class, () ->
+                mBpfNetMaps.getLocalNetAccess(0, TEST_IF_NAME, Inet6Address.ANY, 0, 0));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testGetLocalNetAccessAfterV() throws Exception {
+        assertTrue(mLocalNetAccessMap.isEmpty());
+
+        mLocalNetAccessMap.updateEntry(new LocalNetAccessKey(160, TEST_IF_INDEX,
+                Inet4Address.getByName("196.68.0.0"), 0, 0),
+                new Bool(false));
+
+        assertNotNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+                Inet4Address.getByName("196.68.0.0"), 0, 0)));
+
+        assertFalse(mBpfNetMaps.getLocalNetAccess(160, TEST_IF_NAME,
+                Inet4Address.getByName("196.68.0.0"), 0, 0));
+        assertTrue(mBpfNetMaps.getLocalNetAccess(160, TEST_IF_NAME,
+                Inet4Address.getByName("100.68.0.0"), 0, 0));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testGetLocalNetAccessWithNullInterfaceAfterV() throws Exception {
+        assertTrue(mBpfNetMaps.getLocalNetAccess(160, null,
+                Inet4Address.getByName("100.68.0.0"), 0, 0));
+    }
+
+    @Test
+    @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testRemoveLocalNetAccessBeforeV() {
+        assertThrows(UnsupportedOperationException.class, () ->
+                mBpfNetMaps.removeLocalNetAccess(0, TEST_IF_NAME, Inet6Address.ANY, 0, 0));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testRemoveLocalNetAccessAfterV() throws Exception {
+        assertTrue(mLocalNetAccessMap.isEmpty());
+
+        mBpfNetMaps.addLocalNetAccess(160, TEST_IF_NAME,
+                Inet4Address.getByName("196.68.0.0"), 0, 0, true);
+
+        assertNotNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+                Inet4Address.getByName("196.68.0.0"), 0, 0)));
+        assertNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+                Inet4Address.getByName("100.68.0.0"), 0, 0)));
+
+        mBpfNetMaps.removeLocalNetAccess(160, TEST_IF_NAME,
+                Inet4Address.getByName("196.68.0.0"), 0, 0);
+        assertNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+                Inet4Address.getByName("196.68.0.0"), 0, 0)));
+        assertNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+                Inet4Address.getByName("100.68.0.0"), 0, 0)));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testRemoveLocalNetAccessAfterVWithIncorrectInterface() throws Exception {
+        assertTrue(mLocalNetAccessMap.isEmpty());
+
+        mBpfNetMaps.addLocalNetAccess(160, TEST_IF_NAME,
+                Inet4Address.getByName("196.68.0.0"), 0, 0, true);
+
+        assertNotNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+                Inet4Address.getByName("196.68.0.0"), 0, 0)));
+        assertNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+                Inet4Address.getByName("100.68.0.0"), 0, 0)));
+
+        mBpfNetMaps.removeLocalNetAccess(160, "wlan2",
+                Inet4Address.getByName("196.68.0.0"), 0, 0);
+        assertNotNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+                Inet4Address.getByName("196.68.0.0"), 0, 0)));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testRemoveLocalNetAccessAfterVWithNullInterface() throws Exception {
+        assertTrue(mLocalNetAccessMap.isEmpty());
+
+        mBpfNetMaps.addLocalNetAccess(160, TEST_IF_NAME,
+                Inet4Address.getByName("196.68.0.0"), 0, 0, true);
+
+        assertNotNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+                Inet4Address.getByName("196.68.0.0"), 0, 0)));
+        assertNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+                Inet4Address.getByName("100.68.0.0"), 0, 0)));
+
+        mBpfNetMaps.removeLocalNetAccess(160, null,
+                Inet4Address.getByName("196.68.0.0"), 0, 0);
+        assertNotNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+                Inet4Address.getByName("196.68.0.0"), 0, 0)));
+    }
+
+    @Test
+    @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testAddUidToLocalNetBlockMapBeforeV() {
+        assertThrows(UnsupportedOperationException.class, () ->
+                mBpfNetMaps.addUidToLocalNetBlockMap(0));
+    }
+
+    @Test
+    @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testIsUidBlockedFromUsingLocalNetworkBeforeV() {
+        assertThrows(UnsupportedOperationException.class, () ->
+                mBpfNetMaps.isUidBlockedFromUsingLocalNetwork(0));
+    }
+
+    @Test
+    @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testRemoveUidFromLocalNetBlockMapBeforeV() {
+        assertThrows(UnsupportedOperationException.class, () ->
+                mBpfNetMaps.removeUidFromLocalNetBlockMap(0));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testAddUidFromLocalNetBlockMapAfterV() throws Exception {
+        final int uid0 = TEST_UIDS[0];
+        final int uid1 = TEST_UIDS[1];
+
+        assertTrue(mLocalNetAccessMap.isEmpty());
+
+        mBpfNetMaps.addUidToLocalNetBlockMap(uid0);
+        assertTrue(mLocalNetBlockedUidMap.getValue(new U32(uid0)).val);
+        assertNull(mLocalNetBlockedUidMap.getValue(new U32(uid1)));
+
+        mBpfNetMaps.addUidToLocalNetBlockMap(uid1);
+        assertTrue(mLocalNetBlockedUidMap.getValue(new U32(uid1)).val);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testIsUidBlockedFromUsingLocalNetworkAfterV() throws Exception {
+        final int uid0 = TEST_UIDS[0];
+        final int uid1 = TEST_UIDS[1];
+
+        assertTrue(mLocalNetAccessMap.isEmpty());
+
+        mLocalNetBlockedUidMap.updateEntry(new U32(uid0), new Bool(true));
+        assertTrue(mBpfNetMaps.isUidBlockedFromUsingLocalNetwork(uid0));
+        assertFalse(mBpfNetMaps.isUidBlockedFromUsingLocalNetwork(uid1));
+
+        mLocalNetBlockedUidMap.updateEntry(new U32(uid1), new Bool(true));
+        assertTrue(mBpfNetMaps.isUidBlockedFromUsingLocalNetwork(uid1));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testRemoveUidFromLocalNetBlockMapAfterV() throws Exception {
+        final int uid0 = TEST_UIDS[0];
+        final int uid1 = TEST_UIDS[1];
+
+        assertTrue(mLocalNetAccessMap.isEmpty());
+
+        mLocalNetBlockedUidMap.updateEntry(new U32(uid0), new Bool(true));
+        mLocalNetBlockedUidMap.updateEntry(new U32(uid1), new Bool(true));
+
+        assertTrue(mLocalNetBlockedUidMap.getValue(new U32(uid0)).val);
+        assertTrue(mLocalNetBlockedUidMap.getValue(new U32(uid1)).val);
+
+        mBpfNetMaps.removeUidFromLocalNetBlockMap(uid0);
+        assertNull(mLocalNetBlockedUidMap.getValue(new U32(uid0)));
+        assertTrue(mLocalNetBlockedUidMap.getValue(new U32(uid1)).val);
+
+        mBpfNetMaps.removeUidFromLocalNetBlockMap(uid1);
+        assertNull(mLocalNetBlockedUidMap.getValue(new U32(uid1)));
+    }
+
+    @Test
     @IgnoreUpTo(Build.VERSION_CODES.S_V2)
     public void testIsChainEnabled() throws Exception {
         doTestIsChainEnabled(FIREWALL_CHAIN_DOZABLE);
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index f7d7c87..19a41d8 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -163,10 +163,7 @@
 import static android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH;
 import static android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_LOW;
 
-import static com.android.server.ConnectivityService.ALLOW_SATALLITE_NETWORK_FALLBACK;
 import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
-import static com.android.server.ConnectivityService.ALLOW_SYSUI_CONNECTIVITY_REPORTS;
-import static com.android.server.ConnectivityService.KEY_DESTROY_FROZEN_SOCKETS_VERSION;
 import static com.android.server.ConnectivityService.MAX_NETWORK_REQUESTS_PER_SYSTEM_UID;
 import static com.android.server.ConnectivityService.PREFERENCE_ORDER_MOBILE_DATA_PREFERERRED;
 import static com.android.server.ConnectivityService.PREFERENCE_ORDER_OEM;
@@ -177,9 +174,6 @@
 import static com.android.server.ConnectivityServiceTestUtils.transportToLegacyType;
 import static com.android.server.NetworkAgentWrapper.CallbackType.OnQosCallbackRegister;
 import static com.android.server.NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister;
-import static com.android.server.connectivity.ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN;
-import static com.android.server.connectivity.ConnectivityFlags.DELAY_DESTROY_SOCKETS;
-import static com.android.server.connectivity.ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING;
 import static com.android.testutils.Cleanup.testAndCleanup;
 import static com.android.testutils.ConcurrentUtils.await;
 import static com.android.testutils.ConcurrentUtils.durationOf;
@@ -412,6 +406,7 @@
 import com.android.server.ConnectivityService.NetworkRequestInfo;
 import com.android.server.ConnectivityServiceTest.ConnectivityServiceDependencies.DestroySocketsWrapper;
 import com.android.server.ConnectivityServiceTest.ConnectivityServiceDependencies.ReportedInterfaces;
+import com.android.server.L2capNetworkProvider;
 import com.android.server.connectivity.ApplicationSelfCertifiedNetworkCapabilities;
 import com.android.server.connectivity.AutomaticOnOffKeepaliveTracker;
 import com.android.server.connectivity.CarrierPrivilegeAuthenticator;
@@ -425,6 +420,7 @@
 import com.android.server.connectivity.NetworkAgentInfo;
 import com.android.server.connectivity.NetworkNotificationManager;
 import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
+import com.android.server.connectivity.PermissionMonitor;
 import com.android.server.connectivity.ProxyTracker;
 import com.android.server.connectivity.QosCallbackTracker;
 import com.android.server.connectivity.SatelliteAccessController;
@@ -593,6 +589,7 @@
     private MockContext mServiceContext;
     private HandlerThread mCsHandlerThread;
     private ConnectivityServiceDependencies mDeps;
+    private PermissionMonitorDependencies mPermDeps;
     private AutomaticOnOffKeepaliveTrackerDependencies mAutoOnOffKeepaliveDependencies;
     private ConnectivityService mService;
     private WrappedConnectivityManager mCm;
@@ -1920,6 +1917,7 @@
         doReturn(mResources).when(mockResContext).getResources();
         ConnectivityResources.setResourcesContextForTest(mockResContext);
         mDeps = new ConnectivityServiceDependencies(mockResContext);
+        mPermDeps = new PermissionMonitorDependencies();
         doReturn(true).when(mMockKeepaliveTrackerDependencies)
                 .isAddressTranslationEnabled(mServiceContext);
         doReturn(new ConnectivityResources(mockResContext)).when(mMockKeepaliveTrackerDependencies)
@@ -1932,7 +1930,7 @@
                 mMockDnsResolver,
                 mock(IpConnectivityLog.class),
                 mMockNetd,
-                mDeps);
+                mDeps, mPermDeps);
         mService.mLingerDelayMs = TEST_LINGER_DELAY_MS;
         mService.mNascentDelayMs = TEST_NASCENT_DELAY_MS;
 
@@ -2181,28 +2179,30 @@
                 case ConnectivityFlags.REQUEST_RESTRICTED_WIFI:
                 case ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS:
                 case ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS:
-                case KEY_DESTROY_FROZEN_SOCKETS_VERSION:
+                case ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN:
+                case ConnectivityService.KEY_DESTROY_FROZEN_SOCKETS_VERSION:
                     return true;
                 default:
-                    return super.isFeatureEnabled(context, name);
+                    // This is a unit test and must never depend on actual device flag values.
+                    throw new UnsupportedOperationException("Unknown flag " + name
+                            + ", update this test");
             }
         }
 
         @Override
         public boolean isFeatureNotChickenedOut(Context context, String name) {
             switch (name) {
-                case ALLOW_SYSUI_CONNECTIVITY_REPORTS:
-                    return true;
-                case ALLOW_SATALLITE_NETWORK_FALLBACK:
-                    return true;
-                case INGRESS_TO_VPN_ADDRESS_FILTERING:
-                    return true;
-                case BACKGROUND_FIREWALL_CHAIN:
-                    return true;
-                case DELAY_DESTROY_SOCKETS:
+                case ConnectivityService.ALLOW_SYSUI_CONNECTIVITY_REPORTS:
+                case ConnectivityService.ALLOW_SATALLITE_NETWORK_FALLBACK:
+                case ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING:
+                case ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN:
+                case ConnectivityFlags.DELAY_DESTROY_SOCKETS:
+                case ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS:
+                case ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS:
                     return true;
                 default:
-                    return super.isFeatureNotChickenedOut(context, name);
+                    throw new UnsupportedOperationException("Unknown flag " + name
+                            + ", update this test");
             }
         }
 
@@ -2381,6 +2381,18 @@
             // Needed to mock out the dependency on DeviceConfig
             return 15;
         }
+
+        @Override
+        public L2capNetworkProvider makeL2capNetworkProvider(Context context) {
+            return null;
+        }
+    }
+
+    static class PermissionMonitorDependencies extends PermissionMonitor.Dependencies {
+        @Override
+        public boolean shouldEnforceLocalNetRestrictions(int uid) {
+            return false;
+        }
     }
 
     private class AutomaticOnOffKeepaliveTrackerDependencies
@@ -2431,6 +2443,10 @@
 
     @After
     public void tearDown() throws Exception {
+        // Don't attempt to tear down if setUp didn't even get as far as creating the service.
+        // Otherwise, exceptions here will mask the actual exception in setUp, making failures
+        // harder to diagnose.
+        if (mService == null) return;
         unregisterDefaultNetworkCallbacks();
         maybeTearDownEnterpriseNetwork();
         setAlwaysOnNetworks(false);
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkPermissionsTest.kt b/tests/unit/java/com/android/server/connectivity/NetworkPermissionsTest.kt
new file mode 100644
index 0000000..8a9d288
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/NetworkPermissionsTest.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity
+
+import android.net.INetd
+import android.os.Build
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class NetworkPermissionsTest {
+    @Test
+    fun test_networkTrafficPerms_correctValues() {
+        assertEquals(NetworkPermissions.PERMISSION_NONE, INetd.PERMISSION_NONE) /* 0 */
+        assertEquals(NetworkPermissions.PERMISSION_NETWORK, INetd.PERMISSION_NETWORK) /* 1 */
+        assertEquals(NetworkPermissions.PERMISSION_SYSTEM, INetd.PERMISSION_SYSTEM) /* 2 */
+        assertEquals(NetworkPermissions.TRAFFIC_PERMISSION_INTERNET, 4)
+        assertEquals(NetworkPermissions.TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS, 8)
+        assertEquals(NetworkPermissions.TRAFFIC_PERMISSION_UNINSTALLED, -1)
+        assertEquals(NetworkPermissions.TRAFFIC_PERMISSION_SDKSANDBOX_LOCALHOST, 16)
+    }
+
+    @Test
+    fun test_noOverridesInFlags() {
+        val permsList = listOf(
+            NetworkPermissions.PERMISSION_NONE,
+            NetworkPermissions.PERMISSION_NETWORK,
+            NetworkPermissions.PERMISSION_SYSTEM,
+            NetworkPermissions.TRAFFIC_PERMISSION_INTERNET,
+            NetworkPermissions.TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS,
+            NetworkPermissions.TRAFFIC_PERMISSION_SDKSANDBOX_LOCALHOST,
+            NetworkPermissions.TRAFFIC_PERMISSION_UNINSTALLED
+        )
+        assertFalse(hasDuplicates(permsList))
+    }
+
+    fun hasDuplicates(list: List<Int>): Boolean {
+        return list.distinct().size != list.size
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
index 5bde31a..ec9c6b0 100644
--- a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
@@ -21,7 +21,9 @@
 import static android.Manifest.permission.CONNECTIVITY_INTERNAL;
 import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
 import static android.Manifest.permission.INTERNET;
+import static android.Manifest.permission.NEARBY_WIFI_DEVICES;
 import static android.Manifest.permission.NETWORK_STACK;
+import static android.Manifest.permission.OBSERVE_GRANT_REVOKE_PERMISSIONS;
 import static android.Manifest.permission.UPDATE_DEVICE_STATS;
 import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_OEM;
 import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_PRODUCT;
@@ -30,6 +32,7 @@
 import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_REQUIRED;
 import static android.content.pm.PackageManager.GET_PERMISSIONS;
 import static android.content.pm.PackageManager.MATCH_ANY_USER;
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
 import static android.net.ConnectivitySettingsManager.UIDS_ALLOWED_ON_RESTRICTED_NETWORKS;
 import static android.net.INetd.PERMISSION_INTERNET;
 import static android.net.INetd.PERMISSION_NETWORK;
@@ -38,15 +41,20 @@
 import static android.net.INetd.PERMISSION_UNINSTALLED;
 import static android.net.INetd.PERMISSION_UPDATE_DEVICE_STATS;
 import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
+import static android.net.connectivity.ConnectivityCompatChanges.RESTRICT_LOCAL_NETWORK;
 import static android.os.Process.SYSTEM_UID;
+import static android.permission.PermissionManager.PERMISSION_GRANTED;
 
 import static com.android.server.connectivity.PermissionMonitor.isHigherNetworkPermission;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 
 import static junit.framework.Assert.fail;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
 import static org.mockito.AdditionalMatchers.aryEq;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -66,6 +74,8 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.compat.testing.PlatformCompatChangeRule;
+import android.content.AttributionSource;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -83,7 +93,9 @@
 import android.os.SystemConfigManager;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.permission.PermissionManager;
 import android.provider.Settings;
+import android.util.ArraySet;
 import android.util.SparseIntArray;
 
 import androidx.annotation.NonNull;
@@ -100,9 +112,13 @@
 import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.HandlerUtils;
 
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
 import org.junit.runner.RunWith;
 import org.mockito.AdditionalAnswers;
 import org.mockito.ArgumentCaptor;
@@ -119,6 +135,8 @@
 @SmallTest
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class PermissionMonitorTest {
+    @Rule
+    public TestRule compatChangeRule = new PlatformCompatChangeRule();
     private static final int MOCK_USER_ID1 = 0;
     private static final int MOCK_USER_ID2 = 1;
     private static final int MOCK_USER_ID3 = 2;
@@ -160,9 +178,14 @@
     private static final int PERMISSION_TRAFFIC_ALL =
             PERMISSION_INTERNET | PERMISSION_UPDATE_DEVICE_STATS;
     private static final int TIMEOUT_MS = 2_000;
+    // The ACCESS_LOCAL_NETWORK permission is not available yet. For the time being, use
+    // NEARBY_WIFI_DEVICES as a means to develop, for expediency.
+    // TODO(b/375236298): remove this constant when the ACCESS_LOCAL_NETWORK permission is defined.
+    private static final String ACCESS_LOCAL_NETWORK = NEARBY_WIFI_DEVICES;
 
     @Mock private Context mContext;
     @Mock private PackageManager mPackageManager;
+    @Mock private PermissionManager mPermissionManager;
     @Mock private INetd mNetdService;
     @Mock private UserManager mUserManager;
     @Mock private PermissionMonitor.Dependencies mDeps;
@@ -181,6 +204,7 @@
         when(mContext.getPackageManager()).thenReturn(mPackageManager);
         when(mContext.getSystemService(eq(Context.USER_SERVICE))).thenReturn(mUserManager);
         doReturn(List.of(MOCK_USER1)).when(mUserManager).getUserHandles(eq(true));
+        when(mContext.getSystemService(PermissionManager.class)).thenReturn(mPermissionManager);
         when(mContext.getSystemServiceName(SystemConfigManager.class))
                 .thenReturn(Context.SYSTEM_CONFIG_SERVICE);
         when(mContext.getSystemService(Context.SYSTEM_CONFIG_SERVICE))
@@ -293,19 +317,28 @@
         return result;
     }
 
-    private void buildAndMockPackageInfoWithPermissions(String packageName, int uid,
+    private PackageInfo buildAndMockPackageInfoWithPermissions(String packageName, int uid,
             String... permissions) throws Exception {
         final PackageInfo packageInfo = buildPackageInfo(packageName, uid, permissions);
         // This will return the wrong UID for the package when queried with other users.
         doReturn(packageInfo).when(mPackageManager)
                 .getPackageInfo(eq(packageName), anyInt() /* flag */);
+        if (BpfNetMaps.isAtLeast25Q2()) {
+            // Runtime permission checks for local net restrictions were introduced in 25Q2
+            for (String permission : permissions) {
+                doReturn(PERMISSION_GRANTED).when(mPermissionManager).checkPermissionForPreflight(
+                        eq(permission),
+                        argThat(attributionSource -> attributionSource.getUid() == uid));
+            }
+        }
         final String[] oldPackages = mPackageManager.getPackagesForUid(uid);
         // If it's duplicated package, no need to set it again.
-        if (CollectionUtils.contains(oldPackages, packageName)) return;
+        if (CollectionUtils.contains(oldPackages, packageName)) return packageInfo;
 
         // Combine the package if this uid is shared with other packages.
         final String[] newPackages = appendElement(String.class, oldPackages, packageName);
         doReturn(newPackages).when(mPackageManager).getPackagesForUid(eq(uid));
+        return packageInfo;
     }
 
     private void startMonitoring() {
@@ -340,7 +373,7 @@
 
     private void addPackage(String packageName, int uid, String... permissions) throws Exception {
         buildAndMockPackageInfoWithPermissions(packageName, uid, permissions);
-        processOnHandlerThread(() -> mPermissionMonitor.onPackageAdded(packageName, uid));
+        onPackageAdded(packageName, uid);
     }
 
     private void removePackage(String packageName, int uid) {
@@ -352,7 +385,12 @@
         final String[] newPackages = Arrays.stream(oldPackages).filter(e -> !e.equals(packageName))
                 .toArray(String[]::new);
         doReturn(newPackages).when(mPackageManager).getPackagesForUid(eq(uid));
-        processOnHandlerThread(() -> mPermissionMonitor.onPackageRemoved(packageName, uid));
+        if (BpfNetMaps.isAtLeast25Q2()){
+            // Runtime permission checks for local net restrictions were introduced in 25Q2
+            doReturn(PERMISSION_DENIED).when(mPermissionManager).checkPermissionForPreflight(
+                    anyString(), argThat(as -> as.getUid() == uid));
+        }
+        onPackageRemoved(packageName, uid);
     }
 
     @Test
@@ -583,6 +621,7 @@
     }
 
     @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
     public void testHasUseBackgroundNetworksPermission() throws Exception {
         assertFalse(mPermissionMonitor.hasUseBackgroundNetworksPermission(SYSTEM_UID));
         assertBackgroundPermission(false, SYSTEM_PACKAGE1, SYSTEM_UID);
@@ -604,6 +643,7 @@
 
     private class BpfMapMonitor {
         private final SparseIntArray mAppIdsTrafficPermission = new SparseIntArray();
+        private final ArraySet<Integer> mLocalNetBlockedUids = new ArraySet<>();
         private static final int DOES_NOT_EXIST = -2;
 
         BpfMapMonitor(BpfNetMaps mockBpfmap) throws Exception {
@@ -616,6 +656,18 @@
                 }
                 return null;
             }).when(mockBpfmap).setNetPermForUids(anyInt(), any(int[].class));
+            doAnswer((InvocationOnMock invocation) -> {
+                final Object[] args = invocation.getArguments();
+                final int uid = (int) args[0];
+                mLocalNetBlockedUids.add(uid);
+                return null;
+            }).when(mockBpfmap).addUidToLocalNetBlockMap(anyInt());
+            doAnswer((InvocationOnMock invocation) -> {
+                final Object[] args = invocation.getArguments();
+                final int uid = (int) args[0];
+                mLocalNetBlockedUids.remove(uid);
+                return null;
+            }).when(mockBpfmap).removeUidFromLocalNetBlockMap(anyInt());
         }
 
         public void expectTrafficPerm(int permission, Integer... appIds) {
@@ -640,6 +692,18 @@
                 }
             }
         }
+
+        public boolean hasLocalNetPermissions(int uid) {
+            return !mLocalNetBlockedUids.contains(uid);
+        }
+
+        public boolean isUidPresentInLocalNetBlockMap(int uid) {
+            return mLocalNetBlockedUids.contains(uid);
+        }
+
+        public boolean hasBlockedLocalNetForSandboxUid(int sandboxUid) {
+            return mLocalNetBlockedUids.contains(sandboxUid);
+        }
     }
 
     private class NetdMonitor {
@@ -723,6 +787,7 @@
     }
 
     @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
     public void testUserAndPackageAddRemove() throws Exception {
         // MOCK_UID11: MOCK_PACKAGE1 only has network permission.
         // SYSTEM_APP_UID11: SYSTEM_PACKAGE1 has system permission.
@@ -812,6 +877,48 @@
                 MOCK_APPID1);
     }
 
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
+    public void testLocalNetRestrictions_onUserAdded() throws Exception {
+        assumeTrue(BpfNetMaps.isAtLeast25Q2());
+        doReturn(true).when(mDeps).shouldEnforceLocalNetRestrictions(anyInt());
+        when(mPermissionManager.checkPermissionForPreflight(
+                anyString(), any(AttributionSource.class))).thenReturn(PERMISSION_DENIED);
+        final PackageInfo packageInfo = buildAndMockPackageInfoWithPermissions(
+                MOCK_PACKAGE1, MOCK_UID11, CHANGE_NETWORK_STATE);
+        // Set package for all users on devices
+        doReturn(List.of(packageInfo)).when(mPackageManager)
+                .getInstalledPackagesAsUser(anyInt(), eq(MOCK_USER1.getIdentifier()));
+        onUserAdded(MOCK_USER1);
+
+        assertFalse(mBpfMapMonitor.hasLocalNetPermissions(MOCK_UID11));
+        if (hasSdkSandbox(MOCK_UID11)) {
+            assertTrue(mBpfMapMonitor.hasBlockedLocalNetForSandboxUid(
+                    mProcessShim.toSdkSandboxUid(MOCK_UID11)));
+        }
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
+    public void testLocalNetRestrictions_onUserRemoved() throws Exception {
+        assumeTrue(BpfNetMaps.isAtLeast25Q2());
+        doReturn(true).when(mDeps).shouldEnforceLocalNetRestrictions(anyInt());
+        when(mPermissionManager.checkPermissionForPreflight(
+                anyString(), any(AttributionSource.class))).thenReturn(PERMISSION_DENIED);
+        final PackageInfo packageInfo = buildAndMockPackageInfoWithPermissions(
+                MOCK_PACKAGE1, MOCK_UID11, CHANGE_NETWORK_STATE);
+        // Set package for all users on devices
+        doReturn(List.of(packageInfo)).when(mPackageManager)
+                .getInstalledPackagesAsUser(anyInt(), eq(MOCK_USER1.getIdentifier()));
+        onUserAdded(MOCK_USER1);
+        assertFalse(mBpfMapMonitor.hasLocalNetPermissions(MOCK_UID11));
+
+        onUserRemoved(MOCK_USER1);
+        assertFalse(mBpfMapMonitor.isUidPresentInLocalNetBlockMap(MOCK_UID11));
+    }
+
     private void doTestUidFilteringDuringVpnConnectDisconnectAndUidUpdates(@Nullable String ifName)
             throws Exception {
         doReturn(List.of(
@@ -858,11 +965,13 @@
     }
 
     @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
     public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdates() throws Exception {
         doTestUidFilteringDuringVpnConnectDisconnectAndUidUpdates("tun0");
     }
 
     @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
     public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdatesWithWildcard()
             throws Exception {
         doTestUidFilteringDuringVpnConnectDisconnectAndUidUpdates(null /* ifName */);
@@ -895,16 +1004,19 @@
     }
 
     @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
     public void testUidFilteringDuringPackageInstallAndUninstall() throws Exception {
         doTestUidFilteringDuringPackageInstallAndUninstall("tun0");
     }
 
     @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
     public void testUidFilteringDuringPackageInstallAndUninstallWithWildcard() throws Exception {
         doTestUidFilteringDuringPackageInstallAndUninstall(null /* ifName */);
     }
 
     @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
     public void testLockdownUidFilteringWithLockdownEnableDisable() {
         doReturn(List.of(
                 buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
@@ -936,6 +1048,7 @@
     }
 
     @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
     public void testLockdownUidFilteringWithLockdownEnableDisableWithMultiAdd() {
         doReturn(List.of(
                 buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
@@ -977,6 +1090,7 @@
     }
 
     @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
     public void testLockdownUidFilteringWithLockdownEnableDisableWithMultiAddAndOverlap() {
         doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
                         CONNECTIVITY_USE_RESTRICTED_NETWORKS),
@@ -1037,6 +1151,7 @@
     }
 
     @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
     public void testLockdownUidFilteringWithLockdownEnableDisableWithDuplicates() {
         doReturn(List.of(
                 buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
@@ -1071,6 +1186,7 @@
     }
 
     @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
     public void testLockdownUidFilteringWithInstallAndUnInstall() {
         doReturn(List.of(
                 buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
@@ -1107,15 +1223,13 @@
     // called multiple times with the uid corresponding to each user.
     private void addPackageForUsers(UserHandle[] users, String packageName, int appId) {
         for (final UserHandle user : users) {
-            processOnHandlerThread(() ->
-                    mPermissionMonitor.onPackageAdded(packageName, user.getUid(appId)));
+            onPackageAdded(packageName, user.getUid(appId));
         }
     }
 
     private void removePackageForUsers(UserHandle[] users, String packageName, int appId) {
         for (final UserHandle user : users) {
-            processOnHandlerThread(() ->
-                    mPermissionMonitor.onPackageRemoved(packageName, user.getUid(appId)));
+            onPackageRemoved(packageName, user.getUid(appId));
         }
     }
 
@@ -1163,6 +1277,7 @@
     }
 
     @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
     public void testPackageInstall() throws Exception {
         addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET, UPDATE_DEVICE_STATS);
         mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1);
@@ -1171,7 +1286,25 @@
         mBpfMapMonitor.expectTrafficPerm(PERMISSION_INTERNET, MOCK_APPID2);
     }
 
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
     @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
+    public void testLocalNetRestrictions_onPackageInstall() throws Exception {
+        assumeTrue(BpfNetMaps.isAtLeast25Q2());
+        doReturn(true).when(mDeps).shouldEnforceLocalNetRestrictions(anyInt());
+        when(mPermissionManager.checkPermissionForPreflight(
+                anyString(), any(AttributionSource.class))).thenReturn(PERMISSION_DENIED);
+        addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET);
+        assertFalse(mBpfMapMonitor.hasLocalNetPermissions(MOCK_UID11));
+
+        addPackage(MOCK_PACKAGE2, MOCK_UID12, ACCESS_LOCAL_NETWORK);
+        assertTrue(mBpfMapMonitor.hasLocalNetPermissions(MOCK_UID12));
+        if (hasSdkSandbox(MOCK_UID12)) assertTrue(mBpfMapMonitor.hasBlockedLocalNetForSandboxUid(
+                mProcessShim.toSdkSandboxUid(MOCK_UID12)));
+    }
+
+    @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
     public void testPackageInstallSharedUid() throws Exception {
         addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET, UPDATE_DEVICE_STATS);
         mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1);
@@ -1183,6 +1316,7 @@
     }
 
     @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
     public void testPackageUninstallBasic() throws Exception {
         addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET, UPDATE_DEVICE_STATS);
         mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1);
@@ -1192,7 +1326,24 @@
         mBpfMapMonitor.expectTrafficPerm(PERMISSION_UNINSTALLED, MOCK_APPID1);
     }
 
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
     @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
+    public void testLocalNetRestrictions_onPackageUninstall() throws Exception {
+        assumeTrue(BpfNetMaps.isAtLeast25Q2());
+        doReturn(true).when(mDeps).shouldEnforceLocalNetRestrictions(anyInt());
+        when(mPermissionManager.checkPermissionForPreflight(
+                anyString(), any(AttributionSource.class))).thenReturn(PERMISSION_DENIED);
+        addPackage(MOCK_PACKAGE1, MOCK_UID11, ACCESS_LOCAL_NETWORK);
+        assertTrue(mBpfMapMonitor.hasLocalNetPermissions(MOCK_UID11));
+
+        when(mPackageManager.getPackagesForUid(MOCK_UID11)).thenReturn(new String[]{});
+        onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11);
+        assertFalse(mBpfMapMonitor.isUidPresentInLocalNetBlockMap(MOCK_UID11));
+    }
+
+    @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
     public void testPackageRemoveThenAdd() throws Exception {
         addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET, UPDATE_DEVICE_STATS);
         mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1);
@@ -1205,7 +1356,30 @@
         mBpfMapMonitor.expectTrafficPerm(PERMISSION_INTERNET, MOCK_APPID1);
     }
 
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
     @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
+    public void testLocalNetRestrictions_onPackageRemoveThenAdd() throws Exception {
+        assumeTrue(BpfNetMaps.isAtLeast25Q2());
+        doReturn(true).when(mDeps).shouldEnforceLocalNetRestrictions(anyInt());
+        when(mPermissionManager.checkPermissionForPreflight(
+                anyString(), any(AttributionSource.class))).thenReturn(PERMISSION_DENIED);
+        addPackage(MOCK_PACKAGE1, MOCK_UID11, ACCESS_LOCAL_NETWORK);
+        assertTrue(mBpfMapMonitor.hasLocalNetPermissions(MOCK_UID11));
+        if (hasSdkSandbox(MOCK_UID12)) assertTrue(mBpfMapMonitor.hasBlockedLocalNetForSandboxUid(
+                mProcessShim.toSdkSandboxUid(MOCK_UID11)));
+
+        removePackage(MOCK_PACKAGE1, MOCK_UID11);
+        assertFalse(mBpfMapMonitor.isUidPresentInLocalNetBlockMap(MOCK_UID11));
+
+        addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET);
+        assertFalse(mBpfMapMonitor.hasLocalNetPermissions(MOCK_UID11));
+        if (hasSdkSandbox(MOCK_UID12)) assertTrue(mBpfMapMonitor.hasBlockedLocalNetForSandboxUid(
+                mProcessShim.toSdkSandboxUid(MOCK_UID11)));
+    }
+
+    @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
     public void testPackageUpdate() throws Exception {
         addPackage(MOCK_PACKAGE1, MOCK_UID11);
         mBpfMapMonitor.expectTrafficPerm(PERMISSION_NONE, MOCK_APPID1);
@@ -1215,6 +1389,7 @@
     }
 
     @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
     public void testPackageUninstallWithMultiplePackages() throws Exception {
         addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET, UPDATE_DEVICE_STATS);
         mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1);
@@ -1235,8 +1410,10 @@
         // Use the real context as this test must ensure the *real* system package holds the
         // necessary permission.
         final Context realContext = InstrumentationRegistry.getContext();
-        final PermissionMonitor monitor = new PermissionMonitor(
-                realContext, mNetdService, mBpfNetMaps, mHandlerThread);
+        final PermissionMonitor monitor = runAsShell(
+                OBSERVE_GRANT_REVOKE_PERMISSIONS,
+                () -> new PermissionMonitor(realContext, mNetdService, mBpfNetMaps, mHandlerThread)
+        );
         final PackageManager manager = realContext.getPackageManager();
         final PackageInfo systemInfo = manager.getPackageInfo(REAL_SYSTEM_PACKAGE_NAME,
                 GET_PERMISSIONS | MATCH_ANY_USER);
@@ -1244,6 +1421,7 @@
     }
 
     @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
     public void testUpdateUidPermissionsFromSystemConfig() throws Exception {
         when(mSystemConfigManager.getSystemPermissionUids(eq(INTERNET)))
                 .thenReturn(new int[]{ MOCK_UID11, MOCK_UID12 });
@@ -1283,6 +1461,7 @@
     }
 
     @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
     public void testIntentReceiver() throws Exception {
         startMonitoring();
         final BroadcastReceiver receiver = expectBroadcastReceiver(
@@ -1321,6 +1500,7 @@
     }
 
     @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
     public void testUidsAllowedOnRestrictedNetworksChanged() throws Exception {
         startMonitoring();
         final ContentObserver contentObserver = expectRegisterContentObserver(
@@ -1353,6 +1533,7 @@
     }
 
     @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
     public void testUidsAllowedOnRestrictedNetworksChangedWithSharedUid() throws Exception {
         startMonitoring();
         final ContentObserver contentObserver = expectRegisterContentObserver(
@@ -1386,6 +1567,7 @@
     }
 
     @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
     public void testUidsAllowedOnRestrictedNetworksChangedWithMultipleUsers() throws Exception {
         startMonitoring();
         final ContentObserver contentObserver = expectRegisterContentObserver(
@@ -1440,6 +1622,7 @@
     }
 
     @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
     public void testOnExternalApplicationsAvailable() throws Exception {
         // Initial the permission state. MOCK_PACKAGE1 and MOCK_PACKAGE2 are installed on external
         // and have different uids. There has no permission for both uids.
@@ -1471,6 +1654,7 @@
     }
 
     @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
     public void testOnExternalApplicationsAvailable_AppsNotRegisteredOnStartMonitoring()
             throws Exception {
         startMonitoring();
@@ -1498,6 +1682,7 @@
     }
 
     @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
     public void testOnExternalApplicationsAvailableWithSharedUid()
             throws Exception {
         // Initial the permission state. MOCK_PACKAGE1 and MOCK_PACKAGE2 are installed on external
@@ -1524,6 +1709,7 @@
     }
 
     @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
     public void testOnExternalApplicationsAvailableWithSharedUid_DifferentStorage()
             throws Exception {
         // Initial the permission state. MOCK_PACKAGE1 is installed on external storage and
@@ -1566,6 +1752,38 @@
         assertFalse(isHigherNetworkPermission(PERMISSION_SYSTEM, PERMISSION_SYSTEM));
     }
 
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
+    public void testLocalNetRestrictions_setPermChanges() throws Exception {
+        assumeTrue(BpfNetMaps.isAtLeast25Q2());
+        doReturn(true).when(mDeps).shouldEnforceLocalNetRestrictions(anyInt());
+        when(mPermissionManager.checkPermissionForPreflight(
+                anyString(), any(AttributionSource.class))).thenReturn(PERMISSION_DENIED);
+        addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET);
+        assertFalse(mBpfMapMonitor.hasLocalNetPermissions(MOCK_UID11));
+
+        // Mock permission grant
+        when(mPermissionManager.checkPermissionForPreflight(
+                eq(ACCESS_LOCAL_NETWORK),
+                argThat(attributionSource -> attributionSource.getUid() == MOCK_UID11)))
+                .thenReturn(PERMISSION_GRANTED);
+        mPermissionMonitor.setLocalNetworkPermissions(MOCK_UID11, null);
+        assertTrue(mBpfMapMonitor.hasLocalNetPermissions(MOCK_UID11));
+        if (hasSdkSandbox(MOCK_UID12)) assertTrue(mBpfMapMonitor.hasBlockedLocalNetForSandboxUid(
+                mProcessShim.toSdkSandboxUid(MOCK_UID11)));
+
+        // Mock permission denied
+        when(mPermissionManager.checkPermissionForPreflight(
+                eq(ACCESS_LOCAL_NETWORK),
+                argThat(attributionSource -> attributionSource.getUid() == MOCK_UID11)))
+                .thenReturn(PERMISSION_DENIED);
+        mPermissionMonitor.setLocalNetworkPermissions(MOCK_UID11, null);
+        assertFalse(mBpfMapMonitor.hasLocalNetPermissions(MOCK_UID11));
+        if (hasSdkSandbox(MOCK_UID12)) assertTrue(mBpfMapMonitor.hasBlockedLocalNetForSandboxUid(
+                mProcessShim.toSdkSandboxUid(MOCK_UID11)));
+    }
+
     private void prepareMultiUserPackages() {
         // MOCK_USER1 has installed 3 packages
         // mockApp1 has no permission and share MOCK_APPID1.
@@ -1598,7 +1816,7 @@
 
     private void addUserAndVerifyAppIdsPermissions(UserHandle user, int appId1Perm,
             int appId2Perm, int appId3Perm) {
-        processOnHandlerThread(() -> mPermissionMonitor.onUserAdded(user));
+        onUserAdded(user);
         mBpfMapMonitor.expectTrafficPerm(appId1Perm, MOCK_APPID1);
         mBpfMapMonitor.expectTrafficPerm(appId2Perm, MOCK_APPID2);
         mBpfMapMonitor.expectTrafficPerm(appId3Perm, MOCK_APPID3);
@@ -1606,13 +1824,14 @@
 
     private void removeUserAndVerifyAppIdsPermissions(UserHandle user, int appId1Perm,
             int appId2Perm, int appId3Perm) {
-        processOnHandlerThread(() -> mPermissionMonitor.onUserRemoved(user));
+        onUserRemoved(user);
         mBpfMapMonitor.expectTrafficPerm(appId1Perm, MOCK_APPID1);
         mBpfMapMonitor.expectTrafficPerm(appId2Perm, MOCK_APPID2);
         mBpfMapMonitor.expectTrafficPerm(appId3Perm, MOCK_APPID3);
     }
 
     @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
     public void testAppIdsTrafficPermission_UserAddedRemoved() {
         prepareMultiUserPackages();
 
@@ -1646,6 +1865,7 @@
     }
 
     @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
     public void testAppIdsTrafficPermission_Multiuser_PackageAdded() throws Exception {
         // Add two users with empty package list.
         onUserAdded(MOCK_USER1);
@@ -1716,6 +1936,7 @@
     }
 
     @Test
+    @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
     public void testAppIdsTrafficPermission_Multiuser_PackageRemoved() throws Exception {
         // Add two users with empty package list.
         onUserAdded(MOCK_USER1);
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
index df48f6c..e6e6ecc 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
@@ -42,17 +42,20 @@
 import java.util.Objects
 import java.util.concurrent.CompletableFuture
 import java.util.concurrent.TimeUnit
+import kotlin.test.assertTrue
 import org.junit.After
 import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mockito.any
 import org.mockito.Mockito.anyInt
 import org.mockito.Mockito.argThat
 import org.mockito.Mockito.atLeastOnce
+import org.mockito.Mockito.doCallRealMethod
 import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
@@ -89,6 +92,13 @@
     network = TEST_NETWORK_1
 }
 
+private val GOOGLECAST_SERVICE = NsdServiceInfo("TestServiceName", "_googlecast._tcp").apply {
+    subtypes = setOf(TEST_SUBTYPE)
+    port = 12345
+    hostAddresses = listOf(TEST_ADDR)
+    network = TEST_NETWORK_1
+}
+
 private val SERVICE_1_SUBTYPE = NsdServiceInfo("TestServiceName", "_advertisertest._tcp").apply {
     subtypes = setOf(TEST_SUBTYPE)
     port = 12345
@@ -140,6 +150,15 @@
     OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
 )
 
+private val OFFLOAD_SERVICE_INFO_GOOGLECAST = OffloadServiceInfo(
+    OffloadServiceInfo.Key("TestServiceName", "_googlecast._tcp"),
+    listOf(),
+    "Android_test.local",
+    TEST_OFFLOAD_PACKET1,
+    Int.MAX_VALUE,
+    OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
+)
+
 private val OFFLOAD_SERVICEINFO_NO_SUBTYPE = OffloadServiceInfo(
     OffloadServiceInfo.Key("TestServiceName", "_advertisertest._tcp"),
     listOf(),
@@ -185,12 +204,26 @@
     @Before
     fun setUp() {
         thread.start()
-        doReturn(TEST_HOSTNAME).`when`(mockDeps).generateHostname()
-        doReturn(mockInterfaceAdvertiser1).`when`(mockDeps).makeAdvertiser(eq(mockSocket1),
-                any(), any(), any(), any(), eq(TEST_HOSTNAME), any(), any()
+        doReturn(TEST_HOSTNAME).`when`(mockDeps).generateHostname(anyBoolean())
+        doReturn(mockInterfaceAdvertiser1).`when`(mockDeps).makeAdvertiser(
+            eq(mockSocket1),
+            any(),
+            any(),
+            any(),
+            any(),
+            any(),
+            any(),
+            any()
         )
-        doReturn(mockInterfaceAdvertiser2).`when`(mockDeps).makeAdvertiser(eq(mockSocket2),
-                any(), any(), any(), any(), eq(TEST_HOSTNAME), any(), any()
+        doReturn(mockInterfaceAdvertiser2).`when`(mockDeps).makeAdvertiser(
+            eq(mockSocket2),
+            any(),
+            any(),
+            any(),
+            any(),
+            any(),
+            any(),
+            any()
         )
         doReturn(true).`when`(mockInterfaceAdvertiser1).isProbing(anyInt())
         doReturn(true).`when`(mockInterfaceAdvertiser2).isProbing(anyInt())
@@ -199,16 +232,21 @@
         doReturn(TEST_INTERFACE1).`when`(mockInterfaceAdvertiser1).socketInterfaceName
         doReturn(TEST_INTERFACE2).`when`(mockInterfaceAdvertiser2).socketInterfaceName
         doReturn(TEST_OFFLOAD_PACKET1).`when`(mockInterfaceAdvertiser1).getRawOffloadPayload(
-            SERVICE_ID_1)
+            SERVICE_ID_1
+        )
         doReturn(TEST_OFFLOAD_PACKET1).`when`(mockInterfaceAdvertiser1).getRawOffloadPayload(
-            SERVICE_ID_2)
+            SERVICE_ID_2
+        )
         doReturn(TEST_OFFLOAD_PACKET1).`when`(mockInterfaceAdvertiser1).getRawOffloadPayload(
-            SERVICE_ID_3)
+            SERVICE_ID_3
+        )
         doReturn(TEST_OFFLOAD_PACKET1).`when`(mockInterfaceAdvertiser2).getRawOffloadPayload(
-            SERVICE_ID_1)
+            SERVICE_ID_1
+        )
         doReturn(resources).`when`(context).getResources()
         doReturn(SERVICES_PRIORITY_LIST).`when`(resources).getStringArray(
-            R.array.config_nsdOffloadServicesPriority)
+            R.array.config_nsdOffloadServicesPriority
+        )
         ConnectivityResources.setResourcesContextForTest(context)
     }
 
@@ -229,8 +267,12 @@
     fun testAddService_OneNetwork() {
         val advertiser =
             MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
-        postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
-                DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+        postSync { advertiser.addOrUpdateService(
+            SERVICE_ID_1,
+            SERVICE_1,
+            DEFAULT_ADVERTISING_OPTION,
+            TEST_CLIENT_UID_1
+        ) }
 
         val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
         verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), socketCbCaptor.capture())
@@ -252,7 +294,9 @@
 
         doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
         postSync { intAdvCbCaptor.value.onServiceProbingSucceeded(
-                mockInterfaceAdvertiser1, SERVICE_ID_1) }
+            mockInterfaceAdvertiser1,
+            SERVICE_ID_1
+        ) }
         verify(cb).onRegisterServiceSucceeded(eq(SERVICE_ID_1), argThat { it.matches(SERVICE_1) })
         verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO_NO_SUBTYPE))
 
@@ -293,12 +337,18 @@
     fun testAddService_AllNetworksWithSubType() {
         val advertiser =
             MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
-        postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE_SUBTYPE,
-                DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+        postSync { advertiser.addOrUpdateService(
+            SERVICE_ID_1,
+            ALL_NETWORKS_SERVICE_SUBTYPE,
+            DEFAULT_ADVERTISING_OPTION,
+            TEST_CLIENT_UID_1
+        ) }
 
         val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
-        verify(socketProvider).requestSocket(eq(ALL_NETWORKS_SERVICE_SUBTYPE.network),
-                socketCbCaptor.capture())
+        verify(socketProvider).requestSocket(
+            eq(ALL_NETWORKS_SERVICE_SUBTYPE.network),
+            socketCbCaptor.capture()
+        )
 
         val socketCb = socketCbCaptor.value
         postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
@@ -306,30 +356,56 @@
 
         val intAdvCbCaptor1 = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
         val intAdvCbCaptor2 = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
-        verify(mockDeps).makeAdvertiser(eq(mockSocket1), eq(listOf(TEST_LINKADDR)),
-                eq(thread.looper), any(), intAdvCbCaptor1.capture(), eq(TEST_HOSTNAME), any(), any()
+        verify(mockDeps).makeAdvertiser(
+            eq(mockSocket1),
+            eq(listOf(TEST_LINKADDR)),
+            eq(thread.looper),
+            any(),
+            intAdvCbCaptor1.capture(),
+            eq(TEST_HOSTNAME),
+            any(),
+            any()
         )
-        verify(mockDeps).makeAdvertiser(eq(mockSocket2), eq(listOf(TEST_LINKADDR)),
-                eq(thread.looper), any(), intAdvCbCaptor2.capture(), eq(TEST_HOSTNAME), any(), any()
+        verify(mockDeps).makeAdvertiser(
+            eq(mockSocket2),
+            eq(listOf(TEST_LINKADDR)),
+            eq(thread.looper),
+            any(),
+            intAdvCbCaptor2.capture(),
+            eq(TEST_HOSTNAME),
+            any(),
+            any()
         )
         verify(mockInterfaceAdvertiser1).addService(
-                anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE), any())
+            anyInt(),
+            eq(ALL_NETWORKS_SERVICE_SUBTYPE),
+            any()
+        )
         verify(mockInterfaceAdvertiser2).addService(
-                anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE), any())
+            anyInt(),
+            eq(ALL_NETWORKS_SERVICE_SUBTYPE),
+            any()
+        )
 
         doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
         postSync { intAdvCbCaptor1.value.onServiceProbingSucceeded(
-                mockInterfaceAdvertiser1, SERVICE_ID_1) }
+            mockInterfaceAdvertiser1,
+            SERVICE_ID_1
+        ) }
         verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO))
 
         // Need both advertisers to finish probing and call onRegisterServiceSucceeded
         verify(cb, never()).onRegisterServiceSucceeded(anyInt(), any())
         doReturn(false).`when`(mockInterfaceAdvertiser2).isProbing(SERVICE_ID_1)
         postSync { intAdvCbCaptor2.value.onServiceProbingSucceeded(
-                mockInterfaceAdvertiser2, SERVICE_ID_1) }
+            mockInterfaceAdvertiser2,
+            SERVICE_ID_1
+        ) }
         verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE2), eq(OFFLOAD_SERVICEINFO))
-        verify(cb).onRegisterServiceSucceeded(eq(SERVICE_ID_1),
-                argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) })
+        verify(cb).onRegisterServiceSucceeded(
+            eq(SERVICE_ID_1),
+            argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) }
+        )
 
         // Services are conflicted.
         postSync {
@@ -375,19 +451,30 @@
         val advertiser =
             MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
         postSync {
-            advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1, DEFAULT_ADVERTISING_OPTION,
-                    TEST_CLIENT_UID_1)
-            advertiser.addOrUpdateService(SERVICE_ID_2,
+            advertiser.addOrUpdateService(
+                SERVICE_ID_1,
+                SERVICE_1,
+                DEFAULT_ADVERTISING_OPTION,
+                TEST_CLIENT_UID_1
+            )
+            advertiser.addOrUpdateService(
+                SERVICE_ID_2,
                 NsdServiceInfo("TestService2", "_PRIORITYTEST._udp").apply {
                     port = 12345
                     hostAddresses = listOf(TEST_ADDR)
-                }, DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1)
+                },
+                DEFAULT_ADVERTISING_OPTION,
+                TEST_CLIENT_UID_1
+            )
             advertiser.addOrUpdateService(
                 SERVICE_ID_3,
                 NsdServiceInfo("TestService3", "_notprioritized._tcp").apply {
                     port = 12345
                     hostAddresses = listOf(TEST_ADDR)
-                }, DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1)
+                },
+                DEFAULT_ADVERTISING_OPTION,
+                TEST_CLIENT_UID_1
+            )
         }
 
         val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
@@ -397,8 +484,15 @@
         postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
 
         val intAdvCbCaptor1 = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
-        verify(mockDeps).makeAdvertiser(eq(mockSocket1), eq(listOf(TEST_LINKADDR)),
-            eq(thread.looper), any(), intAdvCbCaptor1.capture(), eq(TEST_HOSTNAME), any(), any()
+        verify(mockDeps).makeAdvertiser(
+            eq(mockSocket1),
+            eq(listOf(TEST_LINKADDR)),
+            eq(thread.looper),
+            any(),
+            intAdvCbCaptor1.capture(),
+            eq(TEST_HOSTNAME),
+            any(),
+            any()
         )
 
         doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
@@ -430,30 +524,88 @@
     }
 
     @Test
+    fun testAddService_NoSubtypeForGoogleCastOffload() {
+        val advertiser =
+            MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
+        postSync {
+            advertiser.addOrUpdateService(
+                SERVICE_ID_1,
+                GOOGLECAST_SERVICE,
+                DEFAULT_ADVERTISING_OPTION,
+                TEST_CLIENT_UID_1
+            )
+        }
+        val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
+        verify(socketProvider).requestSocket(eq(SERVICE_1.network), socketCbCaptor.capture())
+
+        val socketCb = socketCbCaptor.value
+        postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
+
+        val intAdvCbCaptor1 = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
+        verify(mockDeps).makeAdvertiser(
+            eq(mockSocket1),
+            eq(listOf(TEST_LINKADDR)),
+            eq(thread.looper),
+            any(),
+            intAdvCbCaptor1.capture(),
+            eq(TEST_HOSTNAME),
+            any(),
+            any()
+        )
+
+        doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
+        postSync {
+            intAdvCbCaptor1.value.onServiceProbingSucceeded(mockInterfaceAdvertiser1, SERVICE_ID_1)
+        }
+
+        verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICE_INFO_GOOGLECAST))
+    }
+
+    @Test
     fun testAddService_Conflicts() {
         val advertiser =
             MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
-        postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
-                DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+        postSync { advertiser.addOrUpdateService(
+            SERVICE_ID_1,
+            SERVICE_1,
+            DEFAULT_ADVERTISING_OPTION,
+            TEST_CLIENT_UID_1
+        ) }
 
         val oneNetSocketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
         verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), oneNetSocketCbCaptor.capture())
         val oneNetSocketCb = oneNetSocketCbCaptor.value
 
         // Register a service with the same name on all networks (name conflict)
-        postSync { advertiser.addOrUpdateService(SERVICE_ID_2, ALL_NETWORKS_SERVICE,
-                DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+        postSync { advertiser.addOrUpdateService(
+            SERVICE_ID_2,
+            ALL_NETWORKS_SERVICE,
+            DEFAULT_ADVERTISING_OPTION,
+            TEST_CLIENT_UID_1
+        ) }
         val allNetSocketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
         verify(socketProvider).requestSocket(eq(null), allNetSocketCbCaptor.capture())
         val allNetSocketCb = allNetSocketCbCaptor.value
 
-        postSync { advertiser.addOrUpdateService(LONG_SERVICE_ID_1, LONG_SERVICE_1,
-                DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
-        postSync { advertiser.addOrUpdateService(LONG_SERVICE_ID_2, LONG_ALL_NETWORKS_SERVICE,
-                DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+        postSync { advertiser.addOrUpdateService(
+            LONG_SERVICE_ID_1,
+            LONG_SERVICE_1,
+            DEFAULT_ADVERTISING_OPTION,
+            TEST_CLIENT_UID_1
+        ) }
+        postSync { advertiser.addOrUpdateService(
+            LONG_SERVICE_ID_2,
+            LONG_ALL_NETWORKS_SERVICE,
+            DEFAULT_ADVERTISING_OPTION,
+            TEST_CLIENT_UID_1
+        ) }
 
-        postSync { advertiser.addOrUpdateService(CASE_INSENSITIVE_TEST_SERVICE_ID,
-                ALL_NETWORKS_SERVICE_2, DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+        postSync { advertiser.addOrUpdateService(
+            CASE_INSENSITIVE_TEST_SERVICE_ID,
+            ALL_NETWORKS_SERVICE_2,
+            DEFAULT_ADVERTISING_OPTION,
+            TEST_CLIENT_UID_1
+        ) }
 
         // Callbacks for matching network and all networks both get the socket
         postSync {
@@ -462,7 +614,9 @@
         }
 
         val expectedRenamed = NsdServiceInfo(
-                "${ALL_NETWORKS_SERVICE.serviceName} (2)", ALL_NETWORKS_SERVICE.serviceType).apply {
+            "${ALL_NETWORKS_SERVICE.serviceName} (2)",
+            ALL_NETWORKS_SERVICE.serviceType
+        ).apply {
             port = ALL_NETWORKS_SERVICE.port
             hostAddresses = ALL_NETWORKS_SERVICE.hostAddresses
             network = ALL_NETWORKS_SERVICE.network
@@ -470,14 +624,16 @@
 
         val expectedLongRenamed = NsdServiceInfo(
             "${LONG_ALL_NETWORKS_SERVICE.serviceName.dropLast(4)} (2)",
-            LONG_ALL_NETWORKS_SERVICE.serviceType).apply {
+            LONG_ALL_NETWORKS_SERVICE.serviceType
+        ).apply {
             port = LONG_ALL_NETWORKS_SERVICE.port
             hostAddresses = LONG_ALL_NETWORKS_SERVICE.hostAddresses
             network = LONG_ALL_NETWORKS_SERVICE.network
         }
 
         val expectedCaseInsensitiveRenamed = NsdServiceInfo(
-            "${ALL_NETWORKS_SERVICE_2.serviceName} (3)", ALL_NETWORKS_SERVICE_2.serviceType
+            "${ALL_NETWORKS_SERVICE_2.serviceName} (3)",
+            ALL_NETWORKS_SERVICE_2.serviceType
         ).apply {
             port = ALL_NETWORKS_SERVICE_2.port
             hostAddresses = ALL_NETWORKS_SERVICE_2.hostAddresses
@@ -485,30 +641,58 @@
         }
 
         val intAdvCbCaptor = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
-        verify(mockDeps).makeAdvertiser(eq(mockSocket1), eq(listOf(TEST_LINKADDR)),
-                eq(thread.looper), any(), intAdvCbCaptor.capture(), eq(TEST_HOSTNAME), any(), any()
+        verify(mockDeps).makeAdvertiser(
+            eq(mockSocket1),
+            eq(listOf(TEST_LINKADDR)),
+            eq(thread.looper),
+            any(),
+            intAdvCbCaptor.capture(),
+            eq(TEST_HOSTNAME),
+            any(),
+            any()
         )
-        verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1),
-                argThat { it.matches(SERVICE_1) }, any())
-        verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_2),
-                argThat { it.matches(expectedRenamed) }, any())
-        verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_1),
-                argThat { it.matches(LONG_SERVICE_1) }, any())
-        verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_2),
-            argThat { it.matches(expectedLongRenamed) }, any())
-        verify(mockInterfaceAdvertiser1).addService(eq(CASE_INSENSITIVE_TEST_SERVICE_ID),
-            argThat { it.matches(expectedCaseInsensitiveRenamed) }, any())
+        verify(mockInterfaceAdvertiser1).addService(
+            eq(SERVICE_ID_1),
+            argThat { it.matches(SERVICE_1) },
+            any()
+        )
+        verify(mockInterfaceAdvertiser1).addService(
+            eq(SERVICE_ID_2),
+            argThat { it.matches(expectedRenamed) },
+            any()
+        )
+        verify(mockInterfaceAdvertiser1).addService(
+            eq(LONG_SERVICE_ID_1),
+            argThat { it.matches(LONG_SERVICE_1) },
+            any()
+        )
+        verify(mockInterfaceAdvertiser1).addService(
+            eq(LONG_SERVICE_ID_2),
+            argThat { it.matches(expectedLongRenamed) },
+            any()
+        )
+        verify(mockInterfaceAdvertiser1).addService(
+            eq(CASE_INSENSITIVE_TEST_SERVICE_ID),
+            argThat { it.matches(expectedCaseInsensitiveRenamed) },
+            any()
+        )
 
         doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
         postSync { intAdvCbCaptor.value.onServiceProbingSucceeded(
-                mockInterfaceAdvertiser1, SERVICE_ID_1) }
+            mockInterfaceAdvertiser1,
+            SERVICE_ID_1
+        ) }
         verify(cb).onRegisterServiceSucceeded(eq(SERVICE_ID_1), argThat { it.matches(SERVICE_1) })
 
         doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_2)
         postSync { intAdvCbCaptor.value.onServiceProbingSucceeded(
-                mockInterfaceAdvertiser1, SERVICE_ID_2) }
-        verify(cb).onRegisterServiceSucceeded(eq(SERVICE_ID_2),
-                argThat { it.matches(expectedRenamed) })
+            mockInterfaceAdvertiser1,
+            SERVICE_ID_2
+        ) }
+        verify(cb).onRegisterServiceSucceeded(
+            eq(SERVICE_ID_2),
+            argThat { it.matches(expectedRenamed) }
+        )
 
         postSync { oneNetSocketCb.onInterfaceDestroyed(TEST_SOCKETKEY_1, mockSocket1) }
         postSync { allNetSocketCb.onInterfaceDestroyed(TEST_SOCKETKEY_1, mockSocket1) }
@@ -520,10 +704,21 @@
     @Test
     fun testAddOrUpdateService_Updates() {
         val advertiser =
-                MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags,
-                    context)
-        postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE,
-                DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+                MdnsAdvertiser(
+                    thread.looper,
+                    socketProvider,
+                    cb,
+                    mockDeps,
+                    sharedlog,
+                    flags,
+                    context
+                )
+        postSync { advertiser.addOrUpdateService(
+            SERVICE_ID_1,
+            ALL_NETWORKS_SERVICE,
+            DEFAULT_ADVERTISING_OPTION,
+            TEST_CLIENT_UID_1
+        ) }
 
         val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
         verify(socketProvider).requestSocket(eq(null), socketCbCaptor.capture())
@@ -531,41 +726,70 @@
         val socketCb = socketCbCaptor.value
         postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
 
-        verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1),
-                argThat { it.matches(ALL_NETWORKS_SERVICE) }, any())
+        verify(mockInterfaceAdvertiser1).addService(
+            eq(SERVICE_ID_1),
+            argThat { it.matches(ALL_NETWORKS_SERVICE) },
+            any()
+        )
 
         val updateOptions = MdnsAdvertisingOptions.newBuilder().setIsOnlyUpdate(true).build()
 
         // Update with serviceId that is not registered yet should fail
-        postSync { advertiser.addOrUpdateService(SERVICE_ID_2, ALL_NETWORKS_SERVICE_SUBTYPE,
-                updateOptions, TEST_CLIENT_UID_1) }
+        postSync { advertiser.addOrUpdateService(
+            SERVICE_ID_2,
+            ALL_NETWORKS_SERVICE_SUBTYPE,
+            updateOptions,
+            TEST_CLIENT_UID_1
+        ) }
         verify(cb).onRegisterServiceFailed(SERVICE_ID_2, NsdManager.FAILURE_INTERNAL_ERROR)
 
         // Update service with different NsdServiceInfo should fail
-        postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1_SUBTYPE, updateOptions,
-                TEST_CLIENT_UID_1) }
+        postSync { advertiser.addOrUpdateService(
+            SERVICE_ID_1,
+            SERVICE_1_SUBTYPE,
+            updateOptions,
+            TEST_CLIENT_UID_1
+        ) }
         verify(cb).onRegisterServiceFailed(SERVICE_ID_1, NsdManager.FAILURE_INTERNAL_ERROR)
 
         // Update service with same NsdServiceInfo but different subType should succeed
-        postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE_SUBTYPE,
-                updateOptions, TEST_CLIENT_UID_1) }
+        postSync { advertiser.addOrUpdateService(
+            SERVICE_ID_1,
+            ALL_NETWORKS_SERVICE_SUBTYPE,
+            updateOptions,
+            TEST_CLIENT_UID_1
+        ) }
         verify(mockInterfaceAdvertiser1).updateService(eq(SERVICE_ID_1), eq(setOf(TEST_SUBTYPE)))
 
         // Newly created MdnsInterfaceAdvertiser will get addService() call.
         postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_2, mockSocket2, listOf(TEST_LINKADDR2)) }
-        verify(mockInterfaceAdvertiser2).addService(eq(SERVICE_ID_1),
-                argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) }, any())
+        verify(mockInterfaceAdvertiser2).addService(
+            eq(SERVICE_ID_1),
+            argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) },
+            any()
+        )
     }
 
     @Test
     fun testAddOrUpdateService_customTtl_registeredSuccess() {
         val advertiser = MdnsAdvertiser(
-                thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
+            thread.looper,
+            socketProvider,
+            cb,
+            mockDeps,
+            sharedlog,
+            flags,
+            context
+        )
         val updateOptions =
                 MdnsAdvertisingOptions.newBuilder().setTtl(Duration.ofSeconds(30)).build()
 
-        postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE,
-                updateOptions, TEST_CLIENT_UID_1) }
+        postSync { advertiser.addOrUpdateService(
+            SERVICE_ID_1,
+            ALL_NETWORKS_SERVICE,
+            updateOptions,
+            TEST_CLIENT_UID_1
+        ) }
 
         val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
         verify(socketProvider).requestSocket(eq(null), socketCbCaptor.capture())
@@ -578,11 +802,71 @@
     fun testRemoveService_whenAllServiceRemoved_thenUpdateHostName() {
         val advertiser =
             MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
-        verify(mockDeps, times(1)).generateHostname()
-        postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
-                DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+        verify(mockDeps, times(1)).generateHostname(anyBoolean())
+        postSync { advertiser.addOrUpdateService(
+            SERVICE_ID_1,
+            SERVICE_1,
+            DEFAULT_ADVERTISING_OPTION,
+            TEST_CLIENT_UID_1
+        ) }
         postSync { advertiser.removeService(SERVICE_ID_1) }
-        verify(mockDeps, times(2)).generateHostname()
+        verify(mockDeps, times(2)).generateHostname(anyBoolean())
+    }
+
+    private fun doHostnameGenerationTest(shortHostname: Boolean): Array<String> {
+        doCallRealMethod().`when`(mockDeps).generateHostname(anyBoolean())
+        val flags = MdnsFeatureFlags.newBuilder().setIsShortHostnamesEnabled(shortHostname).build()
+        val advertiser =
+            MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
+        postSync { advertiser.addOrUpdateService(
+            SERVICE_ID_1,
+            SERVICE_1,
+            DEFAULT_ADVERTISING_OPTION,
+            TEST_CLIENT_UID_1
+        ) }
+
+        val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
+        verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), socketCbCaptor.capture())
+
+        val socketCb = socketCbCaptor.value
+        postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
+
+        val hostnameCaptor = ArgumentCaptor.forClass(Array<String>::class.java)
+        verify(mockDeps).makeAdvertiser(
+            eq(mockSocket1),
+            eq(listOf(TEST_LINKADDR)),
+            eq(thread.looper),
+            any(),
+            any(),
+            hostnameCaptor.capture(),
+            any(),
+            any()
+        )
+        return hostnameCaptor.value
+    }
+
+    @Test
+    fun testShortHostnameGeneration() {
+        val hostname = doHostnameGenerationTest(shortHostname = true)
+        // Short hostnames are [8 uppercase letters or digits].local
+        assertEquals(2, hostname.size)
+        assertTrue(
+            Regex("Android_[A-Z0-9]{8}").matches(hostname[0]),
+            "Unexpected hostname: ${hostname.contentToString()}"
+        )
+        assertEquals("local", hostname[1])
+    }
+
+    @Test
+    fun testLongHostnameGeneration() {
+        val hostname = doHostnameGenerationTest(shortHostname = false)
+        // Long hostnames are Android_[32 lowercase hex characters].local
+        assertEquals(2, hostname.size)
+        assertTrue(
+            Regex("Android_[a-f0-9]{32}").matches(hostname[0]),
+            "Unexpected hostname: ${hostname.contentToString()}"
+        )
+        assertEquals("local", hostname[1])
     }
 
     private fun postSync(r: () -> Unit) {
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
index ab2fb99..71a3274 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
@@ -442,14 +442,12 @@
             // Verify the checkAndRunOnHandlerThread method
             final CompletableFuture<Boolean> future1 = new CompletableFuture<>();
             executor.checkAndRunOnHandlerThread(()-> future1.complete(true));
-            assertTrue(future1.isDone());
             assertTrue(future1.get(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS));
 
             // Verify the execute method
             final CompletableFuture<Boolean> future2 = new CompletableFuture<>();
             executor.execute(()-> future2.complete(true));
             testableLooper.processAllMessages();
-            assertTrue(future2.isDone());
             assertTrue(future2.get(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS));
 
             // Verify the executeDelayed method
@@ -469,7 +467,6 @@
             // The function should be executed.
             testableLooper.moveTimeForward(500L);
             testableLooper.processAllMessages();
-            assertTrue(future3.isDone());
             assertTrue(future3.get(500L, TimeUnit.MILLISECONDS));
         } finally {
             testableLooper.destroy();
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
index da0bc88..67f9d9c 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -16,13 +16,13 @@
 
 package com.android.server.connectivity.mdns;
 
+import static com.android.server.connectivity.mdns.MdnsQueryScheduler.INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS;
+import static com.android.server.connectivity.mdns.MdnsQueryScheduler.MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS;
+import static com.android.server.connectivity.mdns.MdnsQueryScheduler.TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS;
 import static com.android.server.connectivity.mdns.MdnsSearchOptions.ACTIVE_QUERY_MODE;
 import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE;
 import static com.android.server.connectivity.mdns.MdnsSearchOptions.PASSIVE_QUERY_MODE;
 import static com.android.server.connectivity.mdns.MdnsServiceTypeClient.EVENT_START_QUERYTASK;
-import static com.android.server.connectivity.mdns.QueryTaskConfig.INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS;
-import static com.android.server.connectivity.mdns.QueryTaskConfig.MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS;
-import static com.android.server.connectivity.mdns.QueryTaskConfig.TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS;
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
 import static org.junit.Assert.assertArrayEquals;
@@ -566,21 +566,21 @@
 
         // This is the first query. We will ask for unicast response.
         assertTrue(config.expectUnicastResponse);
-        assertEquals(config.transactionId, 1);
+        assertEquals(config.getTransactionId(), 1);
 
         // For the rest of queries in this burst, we will NOT ask for unicast response.
         for (int i = 1; i < MdnsConfigs.queriesPerBurst(); i++) {
-            int oldTransactionId = config.transactionId;
+            int oldTransactionId = config.getTransactionId();
             config = config.getConfigForNextRun(ACTIVE_QUERY_MODE);
             assertFalse(config.expectUnicastResponse);
-            assertEquals(config.transactionId, oldTransactionId + 1);
+            assertEquals(config.getTransactionId(), oldTransactionId + 1);
         }
 
         // This is the first query of a new burst. We will ask for unicast response.
-        int oldTransactionId = config.transactionId;
+        int oldTransactionId = config.getTransactionId();
         config = config.getConfigForNextRun(ACTIVE_QUERY_MODE);
         assertTrue(config.expectUnicastResponse);
-        assertEquals(config.transactionId, oldTransactionId + 1);
+        assertEquals(config.getTransactionId(), oldTransactionId + 1);
     }
 
     @Test
@@ -591,21 +591,21 @@
 
         // This is the first query. We will ask for unicast response.
         assertTrue(config.expectUnicastResponse);
-        assertEquals(config.transactionId, 1);
+        assertEquals(config.getTransactionId(), 1);
 
         // For the rest of queries in this burst, we will NOT ask for unicast response.
         for (int i = 1; i < MdnsConfigs.queriesPerBurst(); i++) {
-            int oldTransactionId = config.transactionId;
+            int oldTransactionId = config.getTransactionId();
             config = config.getConfigForNextRun(ACTIVE_QUERY_MODE);
             assertFalse(config.expectUnicastResponse);
-            assertEquals(config.transactionId, oldTransactionId + 1);
+            assertEquals(config.getTransactionId(), oldTransactionId + 1);
         }
 
         // This is the first query of a new burst. We will NOT ask for unicast response.
-        int oldTransactionId = config.transactionId;
+        int oldTransactionId = config.getTransactionId();
         config = config.getConfigForNextRun(ACTIVE_QUERY_MODE);
         assertFalse(config.expectUnicastResponse);
-        assertEquals(config.transactionId, oldTransactionId + 1);
+        assertEquals(config.getTransactionId(), oldTransactionId + 1);
     }
 
     @Test
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java
index 1cc9985..f763bae 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java
@@ -610,6 +610,7 @@
 
     @Test
     public void testSocketCreatedForMulticastInterface() throws Exception {
+        doReturn(true).when(mTestNetworkIfaceWrapper).isPointToPoint();
         doReturn(true).when(mTestNetworkIfaceWrapper).supportsMulticast();
         startMonitoringSockets();
 
@@ -621,18 +622,6 @@
     }
 
     @Test
-    public void testNoSocketCreatedForPTPInterface() throws Exception {
-        doReturn(true).when(mTestNetworkIfaceWrapper).isPointToPoint();
-        startMonitoringSockets();
-
-        final TestSocketCallback testCallback = new TestSocketCallback();
-        runOnHandler(() -> mSocketProvider.requestSocket(TEST_NETWORK, testCallback));
-
-        postNetworkAvailable(TRANSPORT_BLUETOOTH);
-        testCallback.expectedNoCallback();
-    }
-
-    @Test
     public void testNoSocketCreatedForVPNInterface() throws Exception {
         // VPN interfaces generally also have IFF_POINTOPOINT, but even if they don't, they should
         // not be included even with TRANSPORT_WIFI.
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
index 8155fd0..06cb7ee 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
@@ -21,7 +21,10 @@
 import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED
 import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED
 import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED
+import android.net.InetAddresses
+import android.net.LinkProperties
 import android.os.Build
+import android.os.Build.VERSION_CODES
 import androidx.test.filters.SmallTest
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
@@ -33,11 +36,32 @@
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.atLeastOnce
 import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.inOrder
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 
+internal val LOCAL_DNS = InetAddresses.parseNumericAddress("224.0.1.2")
+internal val NON_LOCAL_DNS = InetAddresses.parseNumericAddress("76.76.75.75")
+
+private const val IFNAME_1 = "wlan1"
+private const val IFNAME_2 = "wlan2"
+private const val PORT_53 = 53
+private const val PROTOCOL_TCP = 6
+private const val PROTOCOL_UDP = 17
+
+private val lpWithNoLocalDns = LinkProperties().apply {
+    addDnsServer(NON_LOCAL_DNS)
+    interfaceName = IFNAME_1
+}
+
+private val lpWithLocalDns = LinkProperties().apply {
+    addDnsServer(LOCAL_DNS)
+    interfaceName = IFNAME_2
+}
+
 @DevSdkIgnoreRunner.MonitorThreadLeak
 @RunWith(DevSdkIgnoreRunner::class)
 @SmallTest
@@ -69,6 +93,81 @@
         }
     }
 
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    @Test
+    fun testLocalPrefixesUpdatedInBpfMap() {
+        // Connect Wi-Fi network with non-local dns.
+        val wifiAgent = Agent(nc = defaultNc(), lp = lpWithNoLocalDns)
+        wifiAgent.connect()
+
+        // Verify that block rule is added to BpfMap for local prefixes.
+        verify(bpfNetMaps, atLeastOnce()).addLocalNetAccess(any(), eq(IFNAME_1),
+            any(), eq(0), eq(0), eq(false))
+
+        wifiAgent.disconnect()
+        val cellAgent = Agent(nc = defaultNc(), lp = lpWithLocalDns)
+        cellAgent.connect()
+
+        // Verify that block rule is removed from BpfMap for local prefixes.
+        verify(bpfNetMaps, atLeastOnce()).removeLocalNetAccess(any(), eq(IFNAME_1),
+            any(), eq(0), eq(0))
+
+        cellAgent.disconnect()
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    @Test
+    fun testLocalDnsNotUpdatedInBpfMap() {
+        // Connect Wi-Fi network with non-local dns.
+        val wifiAgent = Agent(nc = defaultNc(), lp = lpWithNoLocalDns)
+        wifiAgent.connect()
+
+        // Verify that No allow rule is added to BpfMap since there is no local dns.
+        verify(bpfNetMaps, never()).addLocalNetAccess(any(), any(), any(), any(), any(),
+            eq(true))
+
+        wifiAgent.disconnect()
+        val cellAgent = Agent(nc = defaultNc(), lp = lpWithLocalDns)
+        cellAgent.connect()
+
+        // Verify that No allow rule from port 53 is removed on network change
+        // because no dns was added
+        verify(bpfNetMaps, never()).removeLocalNetAccess(eq(192), eq(IFNAME_1),
+            eq(NON_LOCAL_DNS), any(), eq(PORT_53))
+
+        cellAgent.disconnect()
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    @Test
+    fun testLocalDnsUpdatedInBpfMap() {
+        // Connect Wi-Fi network with one local Dns.
+        val wifiAgent = Agent(nc = defaultNc(), lp = lpWithLocalDns)
+        wifiAgent.connect()
+
+        // Verify that allow rule is added to BpfMap for local dns at port 53,
+        // for TCP(=6) protocol
+        verify(bpfNetMaps, atLeastOnce()).addLocalNetAccess(eq(192), eq(IFNAME_2),
+            eq(LOCAL_DNS), eq(PROTOCOL_TCP), eq(PORT_53), eq(true))
+        // And for UDP(=17) protocol
+        verify(bpfNetMaps, atLeastOnce()).addLocalNetAccess(eq(192), eq(IFNAME_2),
+            eq(LOCAL_DNS), eq(PROTOCOL_UDP), eq(PORT_53), eq(true))
+
+        wifiAgent.disconnect()
+        val cellAgent = Agent(nc = defaultNc(), lp = lpWithNoLocalDns)
+        cellAgent.connect()
+
+        // Verify that allow rule is removed for local dns on network change,
+        // for TCP(=6) protocol
+        verify(bpfNetMaps, atLeastOnce()).removeLocalNetAccess(eq(192), eq(IFNAME_2),
+            eq(LOCAL_DNS), eq(PROTOCOL_TCP), eq(PORT_53))
+        // And for UDP(=17) protocol
+        verify(bpfNetMaps, atLeastOnce()).removeLocalNetAccess(eq(192), eq(IFNAME_2),
+            eq(LOCAL_DNS), eq(PROTOCOL_UDP), eq(PORT_53))
+
+        cellAgent.disconnect()
+    }
+
     private fun mockDataSaverStatus(status: Int) {
         doReturn(status).`when`(context.networkPolicyManager).getRestrictBackgroundStatus(anyInt())
         // While the production code dispatches the intent on the handler thread,
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSDeclaredMethodsForCallbacksTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSDeclaredMethodsForCallbacksTest.kt
index a7083dc..b179aac 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSDeclaredMethodsForCallbacksTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSDeclaredMethodsForCallbacksTest.kt
@@ -150,7 +150,7 @@
         // EXPIRE_LEGACY_REQUEST (=8) is only used in ConnectivityManager and not included.
         // CALLBACK_TRANSITIVE_CALLS_ONLY (=0) is not a callback so not included either.
         assertEquals(
-            "PRECHK|AVAIL|LOSING|LOST|UNAVAIL|NC|LP|SUSP|RESUME|BLK|LOCALINF|0x7fffe101",
+            "PRECHK|AVAIL|LOSING|LOST|UNAVAIL|NC|LP|SUSP|RESUME|BLK|LOCALINF|RES|0x7fffc101",
             ConnectivityService.declaredMethodsFlagsToString(0x7fff_ffff)
         )
         // The toString method and the assertion above need to be updated if constants are added
@@ -158,7 +158,7 @@
             Modifier.isStatic(it.modifiers) && Modifier.isFinal(it.modifiers) &&
                     it.name.startsWith("CALLBACK_")
         }
-        assertEquals(12, constants.size)
+        assertEquals(13, constants.size)
     }
 }
 
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSL2capProviderTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSL2capProviderTest.kt
new file mode 100644
index 0000000..babcba9
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSL2capProviderTest.kt
@@ -0,0 +1,397 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothServerSocket
+import android.bluetooth.BluetoothSocket
+import android.net.INetworkMonitor
+import android.net.INetworkMonitorCallbacks
+import android.net.IpPrefix
+import android.net.L2capNetworkSpecifier
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_6LOWPAN
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_NONE
+import android.net.L2capNetworkSpecifier.ROLE_CLIENT
+import android.net.L2capNetworkSpecifier.ROLE_SERVER
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.MacAddress
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH
+import android.net.NetworkRequest
+import android.net.NetworkSpecifier
+import android.net.RouteInfo
+import android.os.Build
+import android.os.HandlerThread
+import android.os.ParcelFileDescriptor
+import com.android.server.net.L2capNetwork.L2capIpClient
+import com.android.server.net.L2capPacketForwarder
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Reserved
+import com.android.testutils.RecorderCallback.CallbackEntry.Unavailable
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.anyNetwork
+import com.android.testutils.waitForIdle
+import java.io.IOException
+import java.util.Optional
+import java.util.concurrent.LinkedBlockingQueue
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNull
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.ArgumentMatchers.isNull
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.doThrow
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+
+private const val PSM = 0x85
+private val REMOTE_MAC = byteArrayOf(1, 2, 3, 4, 5, 6)
+private val REQUEST = NetworkRequest.Builder()
+        .addTransportType(TRANSPORT_BLUETOOTH)
+        .removeCapability(NET_CAPABILITY_TRUSTED)
+        .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+        .build()
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRunner.MonitorThreadLeak
+class CSL2capProviderTest : CSTest() {
+    private val networkMonitor = mock<INetworkMonitor>()
+
+    private val btAdapter = mock<BluetoothAdapter>()
+    private val btDevice = mock<BluetoothDevice>()
+    private val btServerSocket = mock<BluetoothServerSocket>()
+    private val btSocket = mock<BluetoothSocket>()
+    private val tunInterface = mock<ParcelFileDescriptor>()
+    private val l2capIpClient = mock<L2capIpClient>()
+    private val packetForwarder = mock<L2capPacketForwarder>()
+    private val providerDeps = mock<L2capNetworkProvider.Dependencies>()
+
+    // BlockingQueue does not support put(null) operations, as null is used as an internal sentinel
+    // value. Therefore, use Optional<BluetoothSocket> where an empty optional signals the
+    // BluetoothServerSocket#close() operation.
+    private val acceptQueue = LinkedBlockingQueue<Optional<BluetoothSocket>>()
+
+    private val handlerThread = HandlerThread("CSL2capProviderTest thread").apply { start() }
+    private val registeredCallbacks = ArrayList<TestableNetworkCallback>()
+
+    // Requires Dependencies mock to be setup before creation.
+    private lateinit var provider: L2capNetworkProvider
+
+    @Before
+    fun innerSetUp() {
+        doReturn(btAdapter).`when`(bluetoothManager).getAdapter()
+        doReturn(btServerSocket).`when`(btAdapter).listenUsingInsecureL2capChannel()
+        doReturn(PSM).`when`(btServerSocket).getPsm()
+        doReturn(btDevice).`when`(btAdapter).getRemoteDevice(eq(REMOTE_MAC))
+        doReturn(btSocket).`when`(btDevice).createInsecureL2capChannel(eq(PSM))
+
+        doAnswer {
+            val sock = acceptQueue.take()
+            if (sock == null || !sock.isPresent()) throw IOException()
+            sock.get()
+        }.`when`(btServerSocket).accept()
+
+        doAnswer {
+            acceptQueue.put(Optional.empty())
+        }.`when`(btServerSocket).close()
+
+        doReturn(handlerThread).`when`(providerDeps).getHandlerThread()
+        doReturn(tunInterface).`when`(providerDeps).createTunInterface(any())
+        doReturn(packetForwarder).`when`(providerDeps)
+                .createL2capPacketForwarder(any(), any(), any(), any(), any())
+        doReturn(l2capIpClient).`when`(providerDeps).createL2capIpClient(any(), any(), any())
+
+        val lp = LinkProperties()
+        val ifname = "l2cap-tun0"
+        lp.setInterfaceName(ifname)
+        lp.addLinkAddress(LinkAddress("fe80::1/64"))
+        lp.addRoute(RouteInfo(IpPrefix("fe80::/64"), null /* nextHop */, ifname))
+        doReturn(lp).`when`(l2capIpClient).start()
+
+        // Note: In order to properly register a NetworkAgent, a NetworkMonitor must be created for
+        // the agent. CSAgentWrapper already does some of this, but requires adding additional
+        // Dependencies to the production code. Create a mocked NM inside this test instead.
+        doAnswer { i ->
+            val cb = i.arguments[2] as INetworkMonitorCallbacks
+            cb.onNetworkMonitorCreated(networkMonitor)
+        }.`when`(networkStack).makeNetworkMonitor(
+                any() /* network */,
+                isNull() /* name */,
+                any() /* callbacks */
+        )
+
+        provider = L2capNetworkProvider(providerDeps, context)
+        provider.start()
+    }
+
+    @After
+    fun innerTearDown() {
+        // Unregistering a callback which has previously been unregistered by virtue of receiving
+        // onUnavailable is a noop.
+        registeredCallbacks.forEach { cm.unregisterNetworkCallback(it) }
+        // Wait for CS handler idle, meaning the unregisterNetworkCallback has been processed and
+        // L2capNetworkProvider has been notified.
+        waitForIdle()
+
+        // While quitSafely() effectively waits for idle, it is not enough, because the tear down
+        // path itself posts on the handler thread. This means that waitForIdle() needs to run
+        // twice. The first time, to ensure all active threads have been joined, and the second time
+        // to run all associated clean up actions.
+        handlerThread.waitForIdle(HANDLER_TIMEOUT_MS)
+        handlerThread.quitSafely()
+        handlerThread.join()
+    }
+
+    private fun reserveNetwork(nr: NetworkRequest) = TestableNetworkCallback().also {
+        cm.reserveNetwork(nr, csHandler, it)
+        registeredCallbacks.add(it)
+    }
+
+    private fun requestNetwork(nr: NetworkRequest) = TestableNetworkCallback().also {
+        cm.requestNetwork(nr, it, csHandler)
+        registeredCallbacks.add(it)
+    }
+
+    private fun NetworkRequest.copyWithSpecifier(specifier: NetworkSpecifier): NetworkRequest {
+        // Note: NetworkRequest.Builder(NetworkRequest) *does not* perform a defensive copy but
+        // changes the underlying request.
+        return NetworkRequest.Builder(NetworkRequest(this))
+                .setNetworkSpecifier(specifier)
+                .build()
+    }
+
+    @Test
+    fun testReservation() {
+        val l2capServerSpecifier = L2capNetworkSpecifier.Builder()
+                .setRole(ROLE_SERVER)
+                .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+                .build()
+        val l2capReservation = REQUEST.copyWithSpecifier(l2capServerSpecifier)
+        val reservationCb = reserveNetwork(l2capReservation)
+
+        val reservedCaps = reservationCb.expect<Reserved>().caps
+        val reservedSpec = reservedCaps.networkSpecifier as L2capNetworkSpecifier
+
+        assertEquals(PSM, reservedSpec.getPsm())
+        assertEquals(HEADER_COMPRESSION_6LOWPAN, reservedSpec.headerCompression)
+        assertNull(reservedSpec.remoteAddress)
+
+        reservationCb.assertNoCallback()
+    }
+
+    @Test
+    fun testBlanketOffer_reservationWithoutSpecifier() {
+        reserveNetwork(REQUEST).assertNoCallback()
+    }
+
+    @Test
+    fun testBlanketOffer_reservationWithCorrectSpecifier() {
+        var specifier = L2capNetworkSpecifier.Builder()
+                .setRole(ROLE_SERVER)
+                .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+                .build()
+        var nr = REQUEST.copyWithSpecifier(specifier)
+        reserveNetwork(nr).expect<Reserved>()
+
+        specifier = L2capNetworkSpecifier.Builder()
+                .setRole(ROLE_SERVER)
+                .setHeaderCompression(HEADER_COMPRESSION_NONE)
+                .build()
+        nr = REQUEST.copyWithSpecifier(specifier)
+        reserveNetwork(nr).expect<Reserved>()
+    }
+
+    @Test
+    fun testBlanketOffer_reservationWithIncorrectSpecifier() {
+        var specifier = L2capNetworkSpecifier.Builder().build()
+        var nr = REQUEST.copyWithSpecifier(specifier)
+        reserveNetwork(nr).assertNoCallback()
+
+        specifier = L2capNetworkSpecifier.Builder()
+                .setRole(ROLE_SERVER)
+                .build()
+        nr = REQUEST.copyWithSpecifier(specifier)
+        reserveNetwork(nr).assertNoCallback()
+
+        specifier = L2capNetworkSpecifier.Builder()
+                .setRole(ROLE_SERVER)
+                .setHeaderCompression(HEADER_COMPRESSION_NONE)
+                .setPsm(0x81)
+                .build()
+        nr = REQUEST.copyWithSpecifier(specifier)
+        reserveNetwork(nr).assertNoCallback()
+
+        specifier = L2capNetworkSpecifier.Builder()
+                .setHeaderCompression(HEADER_COMPRESSION_NONE)
+                .build()
+        nr = REQUEST.copyWithSpecifier(specifier)
+        reserveNetwork(nr).assertNoCallback()
+    }
+
+    @Test
+    fun testBluetoothException_listenUsingInsecureL2capChannelThrows() {
+        doThrow(IOException()).`when`(btAdapter).listenUsingInsecureL2capChannel()
+        var specifier = L2capNetworkSpecifier.Builder()
+                .setRole(ROLE_SERVER)
+                .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+                .build()
+        var nr = REQUEST.copyWithSpecifier(specifier)
+        reserveNetwork(nr).expect<Unavailable>()
+
+        doReturn(btServerSocket).`when`(btAdapter).listenUsingInsecureL2capChannel()
+        reserveNetwork(nr).expect<Reserved>()
+    }
+
+    @Test
+    fun testBluetoothException_acceptThrows() {
+        doThrow(IOException()).`when`(btServerSocket).accept()
+        var specifier = L2capNetworkSpecifier.Builder()
+                .setRole(ROLE_SERVER)
+                .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+                .build()
+        var nr = REQUEST.copyWithSpecifier(specifier)
+        val cb = reserveNetwork(nr)
+        cb.expect<Reserved>()
+        cb.expect<Unavailable>()
+
+        // BluetoothServerSocket#close() puts Optional.empty() on the acceptQueue.
+        acceptQueue.clear()
+        doAnswer {
+            val sock = acceptQueue.take()
+            assertFalse(sock.isPresent())
+            throw IOException() // to indicate the socket was closed.
+        }.`when`(btServerSocket).accept()
+        val cb2 = reserveNetwork(nr)
+        cb2.expect<Reserved>()
+        cb2.assertNoCallback()
+    }
+
+    @Test
+    fun testServerNetwork() {
+        val specifier = L2capNetworkSpecifier.Builder()
+                .setRole(ROLE_SERVER)
+                .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+                .build()
+        val nr = REQUEST.copyWithSpecifier(specifier)
+        val cb = reserveNetwork(nr)
+        cb.expect<Reserved>()
+
+        // Unblock BluetoothServerSocket#accept()
+        doReturn(true).`when`(btSocket).isConnected()
+        acceptQueue.put(Optional.of(btSocket))
+
+        cb.expectAvailableCallbacks(anyNetwork(), validated = false)
+        cb.assertNoCallback()
+        // Verify that packet forwarding was started.
+        // TODO: stop mocking L2capPacketForwarder.
+        verify(providerDeps).createL2capPacketForwarder(any(), any(), any(), any(), any())
+    }
+
+    @Test
+    fun testBluetoothException_createInsecureL2capChannelThrows() {
+        doThrow(IOException()).`when`(btDevice).createInsecureL2capChannel(any())
+
+        val specifier = L2capNetworkSpecifier.Builder()
+                .setRole(ROLE_CLIENT)
+                .setHeaderCompression(HEADER_COMPRESSION_NONE)
+                .setRemoteAddress(MacAddress.fromBytes(REMOTE_MAC))
+                .setPsm(PSM)
+                .build()
+        val nr = REQUEST.copyWithSpecifier(specifier)
+        val cb = requestNetwork(nr)
+
+        cb.expect<Unavailable>()
+    }
+
+    @Test
+    fun testBluetoothException_bluetoothSocketConnectThrows() {
+        doThrow(IOException()).`when`(btSocket).connect()
+
+        val specifier = L2capNetworkSpecifier.Builder()
+                .setRole(ROLE_CLIENT)
+                .setHeaderCompression(HEADER_COMPRESSION_NONE)
+                .setRemoteAddress(MacAddress.fromBytes(REMOTE_MAC))
+                .setPsm(PSM)
+                .build()
+        val nr = REQUEST.copyWithSpecifier(specifier)
+        val cb = requestNetwork(nr)
+
+        cb.expect<Unavailable>()
+    }
+
+    @Test
+    fun testClientNetwork() {
+        val specifier = L2capNetworkSpecifier.Builder()
+                .setRole(ROLE_CLIENT)
+                .setHeaderCompression(HEADER_COMPRESSION_NONE)
+                .setRemoteAddress(MacAddress.fromBytes(REMOTE_MAC))
+                .setPsm(PSM)
+                .build()
+        val nr = REQUEST.copyWithSpecifier(specifier)
+        val cb = requestNetwork(nr)
+        cb.expectAvailableCallbacks(anyNetwork(), validated = false)
+    }
+
+    @Test
+    fun testClientNetwork_headerCompressionMismatch() {
+        var specifier = L2capNetworkSpecifier.Builder()
+                .setRole(ROLE_CLIENT)
+                .setHeaderCompression(HEADER_COMPRESSION_NONE)
+                .setRemoteAddress(MacAddress.fromBytes(REMOTE_MAC))
+                .setPsm(PSM)
+                .build()
+        var nr = REQUEST.copyWithSpecifier(specifier)
+        val cb = requestNetwork(nr)
+        cb.expectAvailableCallbacks(anyNetwork(), validated = false)
+
+        specifier = L2capNetworkSpecifier.Builder()
+                .setRole(ROLE_CLIENT)
+                .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+                .setRemoteAddress(MacAddress.fromBytes(REMOTE_MAC))
+                .setPsm(PSM)
+                .build()
+        nr = REQUEST.copyWithSpecifier(specifier)
+        val cb2 = requestNetwork(nr)
+        cb2.expect<Unavailable>()
+    }
+
+    @Test
+    fun testClientNetwork_multipleRequests() {
+        val specifier = L2capNetworkSpecifier.Builder()
+                .setRole(ROLE_CLIENT)
+                .setHeaderCompression(HEADER_COMPRESSION_NONE)
+                .setRemoteAddress(MacAddress.fromBytes(REMOTE_MAC))
+                .setPsm(PSM)
+                .build()
+        val nr = REQUEST.copyWithSpecifier(specifier)
+        val cb = requestNetwork(nr)
+        cb.expectAvailableCallbacks(anyNetwork(), validated = false)
+
+        val cb2 = requestNetwork(nr)
+        cb2.expectAvailableCallbacks(anyNetwork(), validated = false)
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
index cb98454..16a30aa 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
@@ -55,25 +55,28 @@
 @RunWith(DevSdkIgnoreRunner::class)
 @SmallTest
 @IgnoreUpTo(Build.VERSION_CODES.R)
-class CSLocalAgentCreationTests(
-        private val sdkLevel: Int,
-        private val isTv: Boolean,
-        private val addLocalNetCapToRequest: Boolean
-) : CSTest() {
+class CSLocalAgentCreationTests : CSTest() {
+    @Parameterized.Parameter(0) lateinit var params: TestParams
+
+    data class TestParams(
+            val sdkLevel: Int,
+            val isTv: Boolean = false,
+            val addLocalNetCapToRequest: Boolean = true)
+
     companion object {
         @JvmStatic
         @Parameterized.Parameters
         fun arguments() = listOf(
-                arrayOf(VERSION_V, false /* isTv */, true /* addLocalNetCapToRequest */),
-                arrayOf(VERSION_V, false /* isTv */, false /* addLocalNetCapToRequest */),
-                arrayOf(VERSION_V, true /* isTv */, true /* addLocalNetCapToRequest */),
-                arrayOf(VERSION_V, true /* isTv */, false /* addLocalNetCapToRequest */),
-                arrayOf(VERSION_U, false /* isTv */, true /* addLocalNetCapToRequest */),
-                arrayOf(VERSION_U, false /* isTv */, false /* addLocalNetCapToRequest */),
-                arrayOf(VERSION_U, true /* isTv */, true /* addLocalNetCapToRequest */),
-                arrayOf(VERSION_U, true /* isTv */, false /* addLocalNetCapToRequest */),
-                arrayOf(VERSION_T, false /* isTv */, false /* addLocalNetCapToRequest */),
-                arrayOf(VERSION_T, true /* isTv */, false /* addLocalNetCapToRequest */),
+                TestParams(VERSION_V, isTv = false, addLocalNetCapToRequest = true),
+                TestParams(VERSION_V, isTv = false, addLocalNetCapToRequest = false),
+                TestParams(VERSION_V, isTv = true, addLocalNetCapToRequest = true),
+                TestParams(VERSION_V, isTv = true, addLocalNetCapToRequest = false),
+                TestParams(VERSION_U, isTv = false, addLocalNetCapToRequest = true),
+                TestParams(VERSION_U, isTv = false, addLocalNetCapToRequest = false),
+                TestParams(VERSION_U, isTv = true, addLocalNetCapToRequest = true),
+                TestParams(VERSION_U, isTv = true, addLocalNetCapToRequest = false),
+                TestParams(VERSION_T, isTv = false, addLocalNetCapToRequest = false),
+                TestParams(VERSION_T, isTv = true, addLocalNetCapToRequest = false),
         )
     }
 
@@ -84,11 +87,11 @@
     @Test
     fun testLocalAgents() {
         val netdInOrder = inOrder(netd)
-        deps.setBuildSdk(sdkLevel)
-        doReturn(isTv).`when`(packageManager).hasSystemFeature(FEATURE_LEANBACK)
+        deps.setBuildSdk(params.sdkLevel)
+        doReturn(params.isTv).`when`(packageManager).hasSystemFeature(FEATURE_LEANBACK)
         val allNetworksCb = TestableNetworkCallback()
         val request = NetworkRequest.Builder()
-        if (addLocalNetCapToRequest) {
+        if (params.addLocalNetCapToRequest) {
             request.addCapability(NET_CAPABILITY_LOCAL_NETWORK)
         }
         cm.registerNetworkCallback(request.build(), allNetworksCb)
@@ -96,7 +99,8 @@
             addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
             addCapability(NET_CAPABILITY_LOCAL_NETWORK)
         }.build()
-        val localAgent = if (sdkLevel >= VERSION_V || sdkLevel == VERSION_U && isTv) {
+        val localAgent = if (params.sdkLevel >= VERSION_V
+                || params.sdkLevel == VERSION_U && params.isTv) {
             Agent(nc = ncTemplate, score = keepConnectedScore(), lnc = defaultLnc())
         } else {
             assertFailsWith<IllegalArgumentException> { Agent(nc = ncTemplate, lnc = defaultLnc()) }
@@ -106,7 +110,7 @@
         localAgent.connect()
         netdInOrder.verify(netd).networkCreate(
                 makeNativeNetworkConfigLocal(localAgent.network.netId, INetd.PERMISSION_NONE))
-        if (addLocalNetCapToRequest) {
+        if (params.addLocalNetCapToRequest) {
             assertEquals(localAgent.network, allNetworksCb.expect<Available>().network)
         } else {
             allNetworksCb.assertNoCallback(NO_CALLBACK_TIMEOUT_MS)
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
index 83fff87..3583f84 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
@@ -54,9 +54,7 @@
 import org.mockito.Mockito.inOrder
 import org.mockito.Mockito.never
 import org.mockito.Mockito.timeout
-import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyNoMoreInteractions
 
 private const val TIMEOUT_MS = 200L
 private const val MEDIUM_TIMEOUT_MS = 1_000L
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalNetworkProtectionTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalNetworkProtectionTest.kt
new file mode 100644
index 0000000..5bf6e04
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalNetworkProtectionTest.kt
@@ -0,0 +1,516 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License")
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.net.InetAddresses
+import android.net.IpPrefix
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN
+import android.net.NetworkCapabilities.TRANSPORT_VPN
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+private const val LONG_TIMEOUT_MS = 5_000
+private const val PREFIX_LENGTH_IPV4 = 32 + 96
+private const val PREFIX_LENGTH_IPV6 = 32
+private const val WIFI_IFNAME = "wlan0"
+private const val WIFI_IFNAME_2 = "wlan1"
+
+private val wifiNc = NetworkCapabilities.Builder()
+        .addTransportType(TRANSPORT_WIFI)
+        .addCapability(NET_CAPABILITY_INTERNET)
+        .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+        .build()
+
+private fun lp(iface: String, vararg linkAddresses: LinkAddress) = LinkProperties().apply {
+    interfaceName = iface
+    for (linkAddress in linkAddresses) {
+        addLinkAddress(linkAddress)
+    }
+}
+
+private fun nr(transport: Int) = NetworkRequest.Builder()
+        .clearCapabilities()
+        .addTransportType(transport).apply {
+            if (transport != TRANSPORT_VPN) {
+                addCapability(NET_CAPABILITY_NOT_VPN)
+            }
+        }.build()
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+class CSLocalNetworkProtectionTest : CSTest() {
+    private val LOCAL_IPV6_IP_ADDRESS_PREFIX = IpPrefix("fe80::1cf1:35ff:fe8c:db87/64")
+    private val LOCAL_IPV6_LINK_ADDRESS = LinkAddress(
+        LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress(),
+        LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()
+    )
+
+    private val LOCAL_IPV4_IP_ADDRESS_PREFIX_1 = IpPrefix("10.0.0.184/24")
+    private val LOCAL_IPV4_LINK_ADDRRESS_1 =
+        LinkAddress(
+            LOCAL_IPV4_IP_ADDRESS_PREFIX_1.getAddress(),
+            LOCAL_IPV4_IP_ADDRESS_PREFIX_1.getPrefixLength()
+        )
+
+    private val LOCAL_IPV4_IP_ADDRESS_PREFIX_2 = IpPrefix("10.0.255.184/24")
+    private val LOCAL_IPV4_LINK_ADDRRESS_2 =
+        LinkAddress(
+            LOCAL_IPV4_IP_ADDRESS_PREFIX_2.getAddress(),
+            LOCAL_IPV4_IP_ADDRESS_PREFIX_2.getPrefixLength()
+        )
+
+    @Test
+    fun testNetworkWithIPv6LocalAddress_AddressAddedToBpfMap() {
+        val nr = nr(TRANSPORT_WIFI)
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(nr, cb)
+
+        // Connecting to network with IPv6 local address in LinkProperties
+        val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+        val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+        wifiAgent.connect()
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+        // Multicast and Broadcast address should always be populated in local_net_access map
+        verifyPopulationOfMulticastAndBroadcastAddress()
+        // Verifying IPv6 address should be populated in local_net_access map
+        verify(bpfNetMaps).addLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+            eq(WIFI_IFNAME),
+            eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+            eq(0),
+            eq(0),
+            eq(false)
+        )
+    }
+
+    @Test
+    fun testNetworkWithIPv4LocalAddress_AddressAddedToBpfMap() {
+        val nr = nr(TRANSPORT_WIFI)
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(nr, cb)
+
+        val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV4_LINK_ADDRRESS_1)
+        val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+        wifiAgent.connect()
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+        // Multicast and Broadcast address should always be populated in local_net_access map
+        verifyPopulationOfMulticastAndBroadcastAddress()
+
+        // Verifying IPv4 matching prefix(10.0.0.0/8) should be populated in local_net_access map
+        verify(bpfNetMaps).addLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV4 + 8),
+            eq(WIFI_IFNAME),
+            eq(InetAddresses.parseNumericAddress("10.0.0.0")),
+            eq(0),
+            eq(0),
+            eq(false)
+        )
+    }
+
+    @Test
+    fun testChangeLinkPropertiesWithDifferentLinkAddresses_AddressReplacedInBpfMap() {
+        val nr = nr(TRANSPORT_WIFI)
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(nr, cb)
+
+        val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+        val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+        wifiAgent.connect()
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+        // Multicast and Broadcast address should always be populated in local_net_access map
+        verifyPopulationOfMulticastAndBroadcastAddress()
+        // Verifying IPv6 address should be populated in local_net_access map
+        verify(bpfNetMaps).addLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+            eq(WIFI_IFNAME),
+            eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+            eq(0),
+            eq(0),
+            eq(false)
+        )
+
+        // Updating Link Property from IPv6 in Link Address to IPv4 in Link Address
+        val wifiLp2 = lp(WIFI_IFNAME, LOCAL_IPV4_LINK_ADDRRESS_1)
+        wifiAgent.sendLinkProperties(wifiLp2)
+        cb.expect<LinkPropertiesChanged>(wifiAgent.network)
+
+        // Verifying IPv4 matching prefix(10.0.0.0/8) should be populated in local_net_access map
+        verify(bpfNetMaps).addLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV4 + 8),
+            eq(WIFI_IFNAME),
+            eq(InetAddresses.parseNumericAddress("10.0.0.0")),
+            eq(0),
+            eq(0),
+            eq(false)
+        )
+        // Verifying IPv6 address should be removed from local_net_access map
+        verify(bpfNetMaps).removeLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+            eq(WIFI_IFNAME),
+            eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+            eq(0),
+            eq(0)
+        )
+    }
+
+    @Test
+    fun testStackedLinkPropertiesWithDifferentLinkAddresses_AddressAddedInBpfMap() {
+        val nr = nr(TRANSPORT_WIFI)
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(nr, cb)
+
+        val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+        val wifiLp2 = lp(WIFI_IFNAME_2, LOCAL_IPV4_LINK_ADDRRESS_1)
+        // Adding stacked link
+        wifiLp.addStackedLink(wifiLp2)
+        val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+        wifiAgent.connect()
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+        // Multicast and Broadcast address should always be populated in local_net_access map
+        verifyPopulationOfMulticastAndBroadcastAddress()
+        // Verifying IPv6 address should be populated in local_net_access map
+        verify(bpfNetMaps).addLocalNetAccess(
+                eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+                eq(WIFI_IFNAME),
+                eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+                eq(0),
+                eq(0),
+                eq(false)
+        )
+
+        // Multicast and Broadcast address should always be populated on stacked link
+        // in local_net_access map
+        verifyPopulationOfMulticastAndBroadcastAddress(WIFI_IFNAME_2)
+        // Verifying IPv4 matching prefix(10.0.0.0/8) should be populated as part of stacked link
+        // in local_net_access map
+        verify(bpfNetMaps).addLocalNetAccess(
+                eq(PREFIX_LENGTH_IPV4 + 8),
+                eq(WIFI_IFNAME_2),
+                eq(InetAddresses.parseNumericAddress("10.0.0.0")),
+                eq(0),
+                eq(0),
+                eq(false)
+        )
+        // As both addresses are in stacked links, so no address should be removed from the map.
+        verify(bpfNetMaps, never()).removeLocalNetAccess(any(), any(), any(), any(), any())
+    }
+
+    @Test
+    fun testRemovingStackedLinkProperties_AddressRemovedInBpfMap() {
+        val nr = nr(TRANSPORT_WIFI)
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(nr, cb)
+
+        val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+        val wifiLp2 = lp(WIFI_IFNAME_2, LOCAL_IPV4_LINK_ADDRRESS_1)
+        // populating stacked link
+        wifiLp.addStackedLink(wifiLp2)
+        val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+        wifiAgent.connect()
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+        // Multicast and Broadcast address should always be populated in local_net_access map
+        verifyPopulationOfMulticastAndBroadcastAddress()
+        // Verifying IPv6 address should be populated in local_net_access map
+        verify(bpfNetMaps).addLocalNetAccess(
+                eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+                eq(WIFI_IFNAME),
+                eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+                eq(0),
+                eq(0),
+                eq(false)
+        )
+
+        // Multicast and Broadcast address should always be populated on stacked link
+        // in local_net_access map
+        verifyPopulationOfMulticastAndBroadcastAddress(WIFI_IFNAME_2)
+        // Verifying IPv4 matching prefix(10.0.0.0/8) should be populated as part of stacked link
+        // in local_net_access map
+        verify(bpfNetMaps).addLocalNetAccess(
+                eq(PREFIX_LENGTH_IPV4 + 8),
+                eq(WIFI_IFNAME_2),
+                eq(InetAddresses.parseNumericAddress("10.0.0.0")),
+                eq(0),
+                eq(0),
+                eq(false)
+        )
+        // As both addresses are in stacked links, so no address should be removed from the map.
+        verify(bpfNetMaps, never()).removeLocalNetAccess(any(), any(), any(), any(), any())
+
+        // replacing link properties without stacked links
+        val wifiLp_3 = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+        wifiAgent.sendLinkProperties(wifiLp_3)
+        cb.expect<LinkPropertiesChanged>(wifiAgent.network)
+
+        // As both stacked links is removed, 10.0.0.0/8 should be removed from local_net_access map.
+        verify(bpfNetMaps).removeLocalNetAccess(
+                eq(PREFIX_LENGTH_IPV4 + 8),
+                eq(WIFI_IFNAME_2),
+                eq(InetAddresses.parseNumericAddress("10.0.0.0")),
+                eq(0),
+                eq(0)
+        )
+    }
+
+    @Test
+    fun testChangeLinkPropertiesWithLinkAddressesInSameRange_AddressIntactInBpfMap() {
+        val nr = nr(TRANSPORT_WIFI)
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(nr, cb)
+
+        val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV4_LINK_ADDRRESS_1)
+        val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+        wifiAgent.connect()
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+        // Multicast and Broadcast address should always be populated in local_net_access map
+        verifyPopulationOfMulticastAndBroadcastAddress()
+        // Verifying IPv4 matching prefix(10.0.0.0/8) should be populated in local_net_access map
+        verify(bpfNetMaps).addLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV4 + 8),
+            eq(WIFI_IFNAME),
+            eq(LOCAL_IPV4_IP_ADDRESS_PREFIX_1.getAddress()),
+            eq(0),
+            eq(0),
+            eq(false)
+        )
+
+        // Updating Link Property from one IPv4 to another IPv4 within same range(10.0.0.0/8)
+        val wifiLp2 = lp(WIFI_IFNAME, LOCAL_IPV4_LINK_ADDRRESS_2)
+        wifiAgent.sendLinkProperties(wifiLp2)
+        cb.expect<LinkPropertiesChanged>(wifiAgent.network)
+
+        // As both addresses below to same range, so no address should be removed from the map.
+        verify(bpfNetMaps, never()).removeLocalNetAccess(any(), any(), any(), any(), any())
+    }
+
+    @Test
+    fun testChangeLinkPropertiesWithDifferentInterface_AddressReplacedInBpfMap() {
+        val nr = nr(TRANSPORT_WIFI)
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(nr, cb)
+
+        val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+        val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+        wifiAgent.connect()
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+        // Multicast and Broadcast address should always be populated in local_net_access map
+        verifyPopulationOfMulticastAndBroadcastAddress()
+        // Verifying IPv6 address should be populated in local_net_access map
+        verify(bpfNetMaps).addLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+            eq(WIFI_IFNAME),
+            eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+            eq(0),
+            eq(0),
+            eq(false)
+        )
+
+        // Updating Link Property by changing interface name which has IPv4 instead of IPv6
+        val wifiLp2 = lp(WIFI_IFNAME_2, LOCAL_IPV4_LINK_ADDRRESS_1)
+        wifiAgent.sendLinkProperties(wifiLp2)
+        cb.expect<LinkPropertiesChanged>(wifiAgent.network)
+
+        // Multicast and Broadcast address should be populated in local_net_access map for
+        // new interface
+        verifyPopulationOfMulticastAndBroadcastAddress(WIFI_IFNAME_2)
+        // Verifying IPv4 matching prefix(10.0.0.0/8) should be populated in local_net_access map
+        verify(bpfNetMaps).addLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV4 + 8),
+            eq(WIFI_IFNAME_2),
+            eq(InetAddresses.parseNumericAddress("10.0.0.0")),
+            eq(0),
+            eq(0),
+            eq(false)
+        )
+        // Multicast and Broadcast address should be removed in local_net_access map for
+        // old interface
+        verifyRemovalOfMulticastAndBroadcastAddress()
+        // Verifying IPv6 address should be removed from local_net_access map
+        verify(bpfNetMaps).removeLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+            eq(WIFI_IFNAME),
+            eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+            eq(0),
+            eq(0)
+        )
+    }
+
+    @Test
+    fun testAddingAnotherNetwork_AllAddressesAddedInBpfMap() {
+        val nr = nr(TRANSPORT_WIFI)
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(nr, cb)
+
+        val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+        val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+        wifiAgent.connect()
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+        // Multicast and Broadcast address should always be populated in local_net_access map
+        verifyPopulationOfMulticastAndBroadcastAddress()
+        // Verifying IPv6 address should be populated in local_net_access map
+        verify(bpfNetMaps).addLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+            eq(WIFI_IFNAME),
+            eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+            eq(0),
+            eq(0),
+            eq(false)
+        )
+
+        // Adding another network with LinkProperty having IPv4 in LinkAddress
+        val wifiLp2 = lp(WIFI_IFNAME_2, LOCAL_IPV4_LINK_ADDRRESS_1)
+        val wifiAgent2 = Agent(nc = wifiNc, lp = wifiLp2)
+        wifiAgent2.connect()
+
+        // Multicast and Broadcast address should always be populated in local_net_access map
+        verifyPopulationOfMulticastAndBroadcastAddress(WIFI_IFNAME_2)
+        // Verifying IPv4 matching prefix(10.0.0.0/8) should be populated in local_net_access map
+        verify(bpfNetMaps).addLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV4 + 8),
+            eq(WIFI_IFNAME_2),
+            eq(InetAddresses.parseNumericAddress("10.0.0.0")),
+            eq(0),
+            eq(0),
+            eq(false)
+        )
+        // Verifying nothing should be removed from local_net_access map
+        verify(bpfNetMaps, never()).removeLocalNetAccess(any(), any(), any(), any(), any())
+    }
+
+    @Test
+    fun testDestroyingNetwork_AddressesRemovedFromBpfMap() {
+        val nr = nr(TRANSPORT_WIFI)
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(nr, cb)
+
+        val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+        val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+        wifiAgent.connect()
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+        // Multicast and Broadcast address should always be populated in local_net_access map
+        verifyPopulationOfMulticastAndBroadcastAddress()
+        // Verifying IPv6 address should be populated in local_net_access map
+        verify(bpfNetMaps).addLocalNetAccess(
+            eq( PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+            eq(WIFI_IFNAME),
+            eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+            eq(0),
+            eq(0),
+            eq(false)
+        )
+
+        // Unregistering the network
+        wifiAgent.unregisterAfterReplacement(LONG_TIMEOUT_MS)
+        cb.expect<Lost>(wifiAgent.network)
+
+        // Multicast and Broadcast address should be removed in local_net_access map for
+        // old interface
+        verifyRemovalOfMulticastAndBroadcastAddress()
+        // Verifying IPv6 address should be removed from local_net_access map
+        verify(bpfNetMaps).removeLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+            eq(WIFI_IFNAME),
+            eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+            eq(0),
+            eq(0)
+        )
+    }
+
+    // Verify if multicast and broadcast addresses have been added using addLocalNetAccess
+    fun verifyPopulationOfMulticastAndBroadcastAddress(
+        interfaceName: String = WIFI_IFNAME
+    ) {
+        verify(bpfNetMaps).addLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV4 + 4),
+            eq(interfaceName),
+            eq(InetAddresses.parseNumericAddress("224.0.0.0")),
+            eq(0),
+            eq(0),
+            eq(false)
+        )
+        verify(bpfNetMaps).addLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV6 + 8),
+            eq(interfaceName),
+            eq(InetAddresses.parseNumericAddress("ff00::")),
+            eq(0),
+            eq(0),
+            eq(false)
+        )
+        verify(bpfNetMaps).addLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV4 + 32),
+            eq(interfaceName),
+            eq(InetAddresses.parseNumericAddress("255.255.255.255")),
+            eq(0),
+            eq(0),
+            eq(false)
+        )
+    }
+
+    // Verify if multicast and broadcast addresses have been removed using removeLocalNetAccess
+    fun verifyRemovalOfMulticastAndBroadcastAddress(
+        interfaceName: String = WIFI_IFNAME
+    ) {
+        verify(bpfNetMaps).removeLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV4 + 4),
+            eq(interfaceName),
+            eq(InetAddresses.parseNumericAddress("224.0.0.0")),
+            eq(0),
+            eq(0)
+        )
+        verify(bpfNetMaps).removeLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV6 + 8),
+            eq(interfaceName),
+            eq(InetAddresses.parseNumericAddress("ff00::")),
+            eq(0),
+            eq(0)
+        )
+        verify(bpfNetMaps).removeLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV4 + 32),
+            eq(interfaceName),
+            eq(InetAddresses.parseNumericAddress("255.255.255.255")),
+            eq(0),
+            eq(0)
+        )
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSNetworkReservationTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkReservationTest.kt
new file mode 100644
index 0000000..e698930
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkReservationTest.kt
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P
+import android.net.NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_TEST
+import android.net.NetworkProvider
+import android.net.NetworkProvider.NetworkOfferCallback
+import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.os.Build
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Reserved
+import com.android.testutils.RecorderCallback.CallbackEntry.Unavailable
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.TestableNetworkOfferCallback
+import com.android.testutils.TestableNetworkOfferCallback.CallbackEntry.OnNetworkNeeded
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private val ETHERNET_SCORE = NetworkScore.Builder().build()
+private val ETHERNET_CAPS = NetworkCapabilities.Builder()
+        .addTransportType(TRANSPORT_ETHERNET)
+        .addTransportType(TRANSPORT_TEST)
+        .addCapability(NET_CAPABILITY_INTERNET)
+        .addCapability(NET_CAPABILITY_NOT_CONGESTED)
+        .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+        .removeCapability(NET_CAPABILITY_TRUSTED)
+        .build()
+private val BLANKET_CAPS = NetworkCapabilities(ETHERNET_CAPS).apply {
+    reservationId = RES_ID_MATCH_ALL_RESERVATIONS
+}
+private val ETHERNET_REQUEST = NetworkRequest.Builder()
+        .addTransportType(TRANSPORT_ETHERNET)
+        .addTransportType(TRANSPORT_TEST)
+        .removeCapability(NET_CAPABILITY_TRUSTED)
+        .build()
+
+private const val TIMEOUT_MS = 5_000L
+private const val NO_CB_TIMEOUT_MS = 200L
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.R)
+class CSNetworkReservationTest : CSTest() {
+    private lateinit var provider: NetworkProvider
+    private val blanketOffer = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+
+    @Before
+    fun subclassSetUp() {
+        provider = NetworkProvider(context, csHandlerThread.looper, "Ethernet provider")
+        cm.registerNetworkProvider(provider)
+
+        // register a blanket offer for use in tests.
+        provider.registerNetworkOffer(ETHERNET_SCORE, BLANKET_CAPS, blanketOffer)
+    }
+
+    fun NetworkCapabilities.copyWithReservationId(resId: Int) = NetworkCapabilities(this).also {
+        it.reservationId = resId
+    }
+
+    fun NetworkProvider.registerNetworkOffer(
+            score: NetworkScore,
+            caps: NetworkCapabilities,
+            cb: NetworkOfferCallback
+    ) {
+        registerNetworkOffer(score, caps, {r -> r.run()}, cb)
+    }
+
+    @Test
+    fun testReservationRequest() {
+        val cb = TestableNetworkCallback()
+        cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+
+        // validate the reservation matches the blanket offer.
+        val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+        val reservationId = reservationReq.networkCapabilities.reservationId
+
+        // bring up reserved reservation offer
+        val reservedOfferCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+        val reservedOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+        provider.registerNetworkOffer(ETHERNET_SCORE, reservedOfferCaps, reservedOfferCb)
+
+        // validate onReserved was sent to the app
+        val onReservedCaps = cb.expect<Reserved>().caps
+        assertEquals(reservedOfferCaps, onReservedCaps)
+
+        // validate the reservation matches the reserved offer.
+        reservedOfferCb.expectOnNetworkNeeded(reservedOfferCaps)
+
+        // reserved offer goes away
+        provider.unregisterNetworkOffer(reservedOfferCb)
+        cb.expect<Unavailable>()
+    }
+
+    fun TestableNetworkOfferCallback.expectNoCallbackWhere(
+            predicate: (TestableNetworkOfferCallback.CallbackEntry) -> Boolean
+    ) {
+        val event = history.poll(NO_CB_TIMEOUT_MS) { predicate(it) }
+        assertNull(event)
+    }
+
+    @Test
+    fun testReservationRequest_notDeliveredToRegularOffer() {
+        val offerCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+        provider.registerNetworkOffer(ETHERNET_SCORE, ETHERNET_CAPS, {r -> r.run()}, offerCb)
+
+        val cb = TestableNetworkCallback()
+        cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+
+        // validate the offer does not receive onNetworkNeeded for reservation request
+        offerCb.expectNoCallbackWhere {
+            it is OnNetworkNeeded && it.request.type == NetworkRequest.Type.RESERVATION
+        }
+    }
+
+    @Test
+    fun testReservedOffer_preventReservationIdUpdate() {
+        val cb = TestableNetworkCallback()
+        cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+
+        // validate the reservation matches the blanket offer.
+        val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+        val reservationId = reservationReq.networkCapabilities.reservationId
+
+        // bring up reserved offer
+        val reservedCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+        val reservedOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+        provider.registerNetworkOffer(ETHERNET_SCORE, reservedCaps, reservedOfferCb)
+
+        cb.expect<Reserved>()
+        reservedOfferCb.expectOnNetworkNeeded(reservedCaps)
+
+        // try to update the offer's reservationId by reusing the same callback object.
+        // first file a new request to try and match the offer later.
+        val cb2 = TestableNetworkCallback()
+        cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb2)
+
+        val reservationReq2 = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+        val reservationId2 = reservationReq2.networkCapabilities.reservationId
+
+        // try to update the offer's reservationId to an existing reservationId.
+        val updatedCaps = ETHERNET_CAPS.copyWithReservationId(reservationId2)
+        provider.registerNetworkOffer(ETHERNET_SCORE, updatedCaps, reservedOfferCb)
+
+        // validate the original offer disappeared.
+        cb.expect<Unavailable>()
+        // validate the new offer was rejected by CS.
+        reservedOfferCb.expectOnNetworkUnneeded(reservedCaps)
+        // validate cb2 never sees onReserved().
+        cb2.assertNoCallback()
+    }
+
+    @Test
+    fun testReservedOffer_capabilitiesCannotBeUpdated() {
+        val cb = TestableNetworkCallback()
+        cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+
+        val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+        val reservationId = reservationReq.networkCapabilities.reservationId
+
+        val reservedCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+        val reservedOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+        provider.registerNetworkOffer(ETHERNET_SCORE, reservedCaps, reservedOfferCb)
+
+        cb.expect<Reserved>()
+        reservedOfferCb.expectOnNetworkNeeded(reservedCaps)
+
+        // update reserved offer capabilities
+        val updatedCaps = NetworkCapabilities(reservedCaps).addCapability(NET_CAPABILITY_WIFI_P2P)
+        provider.registerNetworkOffer(ETHERNET_SCORE, updatedCaps, reservedOfferCb)
+
+        cb.expect<Unavailable>()
+        reservedOfferCb.expectOnNetworkUnneeded(reservedCaps)
+        reservedOfferCb.assertNoCallback()
+    }
+
+    @Test
+    fun testBlanketOffer_updateAllowed() {
+        val cb = TestableNetworkCallback()
+        cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+        blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS)
+
+        val updatedCaps = NetworkCapabilities(BLANKET_CAPS).addCapability(NET_CAPABILITY_WIFI_P2P)
+        provider.registerNetworkOffer(ETHERNET_SCORE, updatedCaps, blanketOffer)
+        blanketOffer.assertNoCallback()
+
+        // Note: NetworkRequest.Builder(NetworkRequest) *does not* perform a defensive copy but
+        // changes the underlying request.
+        val p2pRequest = NetworkRequest.Builder(NetworkRequest(ETHERNET_REQUEST))
+                .addCapability(NET_CAPABILITY_WIFI_P2P)
+                .build()
+        cm.reserveNetwork(p2pRequest, csHandler, cb)
+        blanketOffer.expectOnNetworkNeeded(updatedCaps)
+    }
+
+    @Test
+    fun testReservationOffer_onlyAllowSingleOffer() {
+        val cb = TestableNetworkCallback()
+        cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+
+        val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+        val reservationId = reservationReq.networkCapabilities.reservationId
+
+        val offerCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+        val caps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+        provider.registerNetworkOffer(ETHERNET_SCORE, caps, offerCb)
+        offerCb.expectOnNetworkNeeded(caps)
+        cb.expect<Reserved>()
+
+        val newOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+        provider.registerNetworkOffer(ETHERNET_SCORE, caps, newOfferCb)
+        newOfferCb.assertNoCallback()
+        cb.assertNoCallback()
+
+        // File a regular request and validate only the old offer gets onNetworkNeeded.
+        val cb2 = TestableNetworkCallback()
+        cm.requestNetwork(ETHERNET_REQUEST, cb2, csHandler)
+        offerCb.expectOnNetworkNeeded(caps)
+        newOfferCb.assertNoCallback()
+    }
+
+    @Test
+    fun testReservationOffer_updateScore() {
+        val cb = TestableNetworkCallback()
+        cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+
+        val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+        val reservationId = reservationReq.networkCapabilities.reservationId
+
+        val reservedCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+        val reservedOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+        provider.registerNetworkOffer(ETHERNET_SCORE, reservedCaps, reservedOfferCb)
+        reservedOfferCb.expectOnNetworkNeeded(reservedCaps)
+        reservedOfferCb.assertNoCallback()
+        cb.expect<Reserved>()
+
+        // update reserved offer capabilities
+        val newScore = NetworkScore.Builder().setShouldYieldToBadWifi(true).build()
+        provider.registerNetworkOffer(newScore, reservedCaps, reservedOfferCb)
+        cb.assertNoCallback()
+
+        val cb2 = TestableNetworkCallback()
+        cm.requestNetwork(ETHERNET_REQUEST, cb2, csHandler)
+        reservedOfferCb.expectOnNetworkNeeded(reservedCaps)
+        reservedOfferCb.assertNoCallback()
+    }
+
+    @Test
+    fun testReservationOffer_regularOfferCanBeUpdated() {
+        val offerCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+        provider.registerNetworkOffer(ETHERNET_SCORE, ETHERNET_CAPS, offerCb)
+
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(ETHERNET_REQUEST, cb, csHandler)
+        offerCb.expectOnNetworkNeeded(ETHERNET_CAPS)
+        offerCb.assertNoCallback()
+
+        val updatedCaps = NetworkCapabilities(ETHERNET_CAPS).addCapability(NET_CAPABILITY_WIFI_P2P)
+        val newScore = NetworkScore.Builder().setShouldYieldToBadWifi(true).build()
+        provider.registerNetworkOffer(newScore, updatedCaps, offerCb)
+        offerCb.assertNoCallback()
+
+        val cb2 = TestableNetworkCallback()
+        cm.requestNetwork(ETHERNET_REQUEST, cb2, csHandler)
+        offerCb.expectOnNetworkNeeded(ETHERNET_CAPS)
+        offerCb.assertNoCallback()
+    }
+}
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 ae196a6..557bfd6 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -18,6 +18,7 @@
 
 import android.app.AlarmManager
 import android.app.AppOpsManager
+import android.bluetooth.BluetoothManager
 import android.content.BroadcastReceiver
 import android.content.Context
 import android.content.Intent
@@ -71,6 +72,7 @@
 import com.android.server.connectivity.MultinetworkPolicyTracker
 import com.android.server.connectivity.MultinetworkPolicyTrackerTestDependencies
 import com.android.server.connectivity.NetworkRequestStateStatsMetrics
+import com.android.server.connectivity.PermissionMonitor
 import com.android.server.connectivity.ProxyTracker
 import com.android.server.connectivity.SatelliteAccessController
 import com.android.testutils.visibleOnHandlerThread
@@ -209,12 +211,14 @@
         doReturn(true).`when`(it).isDataCapable()
     }
     val subscriptionManager = mock<SubscriptionManager>()
+    val bluetoothManager = mock<BluetoothManager>()
 
     val multicastRoutingCoordinatorService = mock<MulticastRoutingCoordinatorService>()
     val satelliteAccessController = mock<SatelliteAccessController>()
     val destroySocketsWrapper = mock<DestroySocketsWrapper>()
 
     val deps = CSDeps()
+    val permDeps = PermDeps()
 
     // Initializations that start threads are done from setUp to avoid thread leak
     lateinit var alarmHandlerThread: HandlerThread
@@ -251,7 +255,9 @@
 
         alarmHandlerThread = HandlerThread("TestAlarmManager").also { it.start() }
         alarmManager = makeMockAlarmManager(alarmHandlerThread)
-        service = makeConnectivityService(context, netd, deps).also { it.systemReadyInternal() }
+        service = makeConnectivityService(context, netd, deps, permDeps).also {
+            it.systemReadyInternal()
+        }
         cm = ConnectivityManager(context, service)
         // csHandler initialization must be after makeConnectivityService since ConnectivityService
         // constructor starts csHandlerThread
@@ -393,6 +399,12 @@
             // Call mocked destroyLiveTcpSocketsByOwnerUids so that test can verify this method call
             destroySocketsWrapper.destroyLiveTcpSocketsByOwnerUids(ownerUids)
         }
+
+        override fun makeL2capNetworkProvider(context: Context) = null
+    }
+
+    inner class PermDeps : PermissionMonitor.Dependencies() {
+        override fun shouldEnforceLocalNetRestrictions(uid: Int) = false
     }
 
     inner class CSContext(base: Context) : BroadcastInterceptingContext(base) {
@@ -503,6 +515,7 @@
             Context.BATTERY_STATS_SERVICE -> batteryManager
             Context.STATS_MANAGER -> null // Stats manager is final and can't be mocked
             Context.APP_OPS_SERVICE -> appOpsManager
+            Context.BLUETOOTH_SERVICE -> bluetoothManager
             else -> super.getSystemService(serviceName)
         }
 
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 8ff790c..a53d430 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
@@ -23,6 +23,7 @@
 import android.content.Context
 import android.content.pm.PackageManager
 import android.content.pm.PackageManager.FEATURE_BLUETOOTH
+import android.content.pm.PackageManager.FEATURE_BLUETOOTH_LE
 import android.content.pm.PackageManager.FEATURE_ETHERNET
 import android.content.pm.PackageManager.FEATURE_WIFI
 import android.content.pm.PackageManager.FEATURE_WIFI_DIRECT
@@ -53,6 +54,7 @@
 import com.android.modules.utils.build.SdkLevel
 import com.android.server.ConnectivityService.Dependencies
 import com.android.server.connectivity.ConnectivityResources
+import com.android.server.connectivity.PermissionMonitor
 import kotlin.test.fail
 import org.mockito.ArgumentMatchers
 import org.mockito.ArgumentMatchers.any
@@ -103,7 +105,13 @@
 }
 
 internal fun makeMockPackageManager(realContext: Context) = mock<PackageManager>().also { pm ->
-    val supported = listOf(FEATURE_WIFI, FEATURE_WIFI_DIRECT, FEATURE_BLUETOOTH, FEATURE_ETHERNET)
+    val supported = listOf(
+            FEATURE_WIFI,
+            FEATURE_WIFI_DIRECT,
+            FEATURE_BLUETOOTH,
+            FEATURE_BLUETOOTH_LE,
+            FEATURE_ETHERNET
+    )
     doReturn(true).`when`(pm).hasSystemFeature(argThat { supported.contains(it) })
     val myPackageName = realContext.packageName
     val myPackageInfo = realContext.packageManager.getPackageInfo(myPackageName,
@@ -185,13 +193,14 @@
 
 private val TEST_LINGER_DELAY_MS = 400
 private val TEST_NASCENT_DELAY_MS = 300
-internal fun makeConnectivityService(context: Context, netd: INetd, deps: Dependencies) =
+internal fun makeConnectivityService(context: Context, netd: INetd, deps: Dependencies,
+                                     mPermDeps: PermissionMonitor.Dependencies) =
         ConnectivityService(
                 context,
                 mock<IDnsResolver>(),
                 mock<IpConnectivityLog>(),
                 netd,
-                deps).also {
+                deps, mPermDeps).also {
             it.mLingerDelayMs = TEST_LINGER_DELAY_MS
             it.mNascentDelayMs = TEST_NASCENT_DELAY_MS
         }
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
index e6aba22..533bbf8 100644
--- a/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
+++ b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
@@ -79,6 +79,7 @@
         initMockResources();
         doReturn(false).when(mFactory).updateInterfaceLinkState(anyString(), anyBoolean());
         doReturn(new String[0]).when(mNetd).interfaceGetList();
+        doReturn(new String[0]).when(mFactory).getAvailableInterfaces(anyBoolean());
         mHandlerThread = new HandlerThread(THREAD_NAME);
         mHandlerThread.start();
         tracker = new EthernetTracker(mContext, mHandlerThread.getThreadHandler(), mFactory, mNetd,
@@ -166,9 +167,10 @@
                 EthernetTracker.parseStaticIpConfiguration(configAsString));
     }
 
-    private NetworkCapabilities.Builder makeEthernetCapabilitiesBuilder(boolean clearAll) {
+    private NetworkCapabilities.Builder makeEthernetCapabilitiesBuilder(boolean clearDefaults) {
         final NetworkCapabilities.Builder builder =
-                clearAll ? NetworkCapabilities.Builder.withoutDefaultCapabilities()
+                clearDefaults
+                        ? NetworkCapabilities.Builder.withoutDefaultCapabilities()
                         : new NetworkCapabilities.Builder();
         return builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
                 .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED)
@@ -176,21 +178,20 @@
     }
 
     /**
-     * Test: Attempt to create a capabilties with various valid sets of capabilities/transports
+     * Test: Attempt to create a capabilities with various valid sets of capabilities/transports
      */
     @Test
     public void createNetworkCapabilities() {
-
         // Particularly common expected results
-        NetworkCapabilities defaultEthernetCleared =
-                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+        NetworkCapabilities defaultCapabilities =
+                makeEthernetCapabilitiesBuilder(false /* clearDefaults */)
                         .setLinkUpstreamBandwidthKbps(100000)
                         .setLinkDownstreamBandwidthKbps(100000)
                         .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
                         .build();
 
         NetworkCapabilities ethernetClearedWithCommonCaps =
-                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                makeEthernetCapabilitiesBuilder(true /* clearDefaults */)
                         .setLinkUpstreamBandwidthKbps(100000)
                         .setLinkDownstreamBandwidthKbps(100000)
                         .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
@@ -200,89 +201,71 @@
                         .addCapability(15)
                         .build();
 
-        // Empty capabilities and transports lists with a "please clear defaults" should
-        // yield an empty capabilities set with TRANPORT_ETHERNET
-        assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "");
+        // Empty capabilities and transports should return the default capabilities set
+        // with TRANSPORT_ETHERNET
+        assertParsedNetworkCapabilities(defaultCapabilities, "", "");
 
-        // Empty capabilities and transports without the clear defaults flag should return the
-        // default capabilities set with TRANSPORT_ETHERNET
-        assertParsedNetworkCapabilities(
-                makeEthernetCapabilitiesBuilder(false /* clearAll */)
-                        .setLinkUpstreamBandwidthKbps(100000)
-                        .setLinkDownstreamBandwidthKbps(100000)
-                        .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
-                        .build(),
-                false, "", "");
-
-        // A list of capabilities without the clear defaults flag should return the default
-        // capabilities, mixed with the desired capabilities, and TRANSPORT_ETHERNET
-        assertParsedNetworkCapabilities(
-                makeEthernetCapabilitiesBuilder(false /* clearAll */)
-                        .setLinkUpstreamBandwidthKbps(100000)
-                        .setLinkDownstreamBandwidthKbps(100000)
-                        .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
-                        .addCapability(11)
-                        .addCapability(12)
-                        .build(),
-                false, "11,12", "");
-
-        // Adding a list of capabilities with a clear defaults will leave exactly those capabilities
-        // with a default TRANSPORT_ETHERNET since no overrides are specified
-        assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15", "");
+        // Adding a list of capabilities will leave exactly those capabilities with a default
+        // TRANSPORT_ETHERNET since no overrides are specified
+        assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, "12,13,14,15", "");
 
         // Adding any invalid capabilities to the list will cause them to be ignored
-        assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15,65,73", "");
-        assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15,abcdefg", "");
+        assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, "12,13,14,15,65,73", "");
+        assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, "12,13,14,15,abcdefg", "");
 
         // Adding a valid override transport will remove the default TRANSPORT_ETHERNET transport
-        // and apply only the override to the capabiltities object
+        // and apply only the override to the capabilities object
         assertParsedNetworkCapabilities(
-                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                makeEthernetCapabilitiesBuilder(false /* clearDefaults */)
                         .setLinkUpstreamBandwidthKbps(100000)
                         .setLinkDownstreamBandwidthKbps(100000)
                         .addTransportType(0)
                         .build(),
-                true, "", "0");
+                "",
+                "0");
         assertParsedNetworkCapabilities(
-                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                makeEthernetCapabilitiesBuilder(false /* clearDefaults */)
                         .setLinkUpstreamBandwidthKbps(100000)
                         .setLinkDownstreamBandwidthKbps(100000)
                         .addTransportType(1)
                         .build(),
-                true, "", "1");
+                "",
+                "1");
         assertParsedNetworkCapabilities(
-                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                makeEthernetCapabilitiesBuilder(false /* clearDefaults */)
                         .setLinkUpstreamBandwidthKbps(100000)
                         .setLinkDownstreamBandwidthKbps(100000)
                         .addTransportType(2)
                         .build(),
-                true, "", "2");
+                "",
+                "2");
         assertParsedNetworkCapabilities(
-                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                makeEthernetCapabilitiesBuilder(false /* clearDefaults */)
                         .setLinkUpstreamBandwidthKbps(100000)
                         .setLinkDownstreamBandwidthKbps(100000)
                         .addTransportType(3)
                         .build(),
-                true, "", "3");
+                "",
+                "3");
 
-        // "4" is TRANSPORT_VPN, which is unsupported. Should default back to TRANPORT_ETHERNET
-        assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "4");
+        // "4" is TRANSPORT_VPN, which is unsupported. Should default back to TRANSPORT_ETHERNET
+        assertParsedNetworkCapabilities(defaultCapabilities, "", "4");
 
         // "5" is TRANSPORT_WIFI_AWARE, which is currently supported due to no legacy TYPE_NONE
         // conversion. When that becomes available, this test must be updated
-        assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "5");
+        assertParsedNetworkCapabilities(defaultCapabilities, "", "5");
 
         // "6" is TRANSPORT_LOWPAN, which is currently supported due to no legacy TYPE_NONE
         // conversion. When that becomes available, this test must be updated
-        assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "6");
+        assertParsedNetworkCapabilities(defaultCapabilities, "", "6");
 
         // Adding an invalid override transport will leave the transport as TRANSPORT_ETHERNET
-        assertParsedNetworkCapabilities(defaultEthernetCleared,true, "", "100");
-        assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "abcdefg");
+        assertParsedNetworkCapabilities(defaultCapabilities, "", "100");
+        assertParsedNetworkCapabilities(defaultCapabilities, "", "abcdefg");
 
         // Ensure the adding of both capabilities and transports work
         assertParsedNetworkCapabilities(
-                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                makeEthernetCapabilitiesBuilder(true /* clearDefaults */)
                         .setLinkUpstreamBandwidthKbps(100000)
                         .setLinkDownstreamBandwidthKbps(100000)
                         .addCapability(12)
@@ -291,17 +274,21 @@
                         .addCapability(15)
                         .addTransportType(3)
                         .build(),
-                true, "12,13,14,15", "3");
+                "12,13,14,15",
+                "3");
 
         // Ensure order does not matter for capability list
-        assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "13,12,15,14", "");
+        assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, "13,12,15,14", "");
     }
 
-    private void assertParsedNetworkCapabilities(NetworkCapabilities expectedNetworkCapabilities,
-            boolean clearCapabilties, String configCapabiltiies,String configTransports) {
-        assertEquals(expectedNetworkCapabilities,
-                EthernetTracker.createNetworkCapabilities(clearCapabilties, configCapabiltiies,
-                        configTransports).build());
+    private void assertParsedNetworkCapabilities(
+            NetworkCapabilities expectedNetworkCapabilities,
+            String configCapabiltiies,
+            String configTransports) {
+        assertEquals(
+                expectedNetworkCapabilities,
+                EthernetTracker.createNetworkCapabilities(configCapabiltiies, configTransports)
+                        .build());
     }
 
     @Test
diff --git a/tests/unit/java/com/android/server/net/HeaderCompressionUtilsTest.kt b/tests/unit/java/com/android/server/net/HeaderCompressionUtilsTest.kt
new file mode 100644
index 0000000..8431194
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/HeaderCompressionUtilsTest.kt
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net
+
+import android.os.Build
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.internal.util.HexDump
+import com.google.common.truth.Truth.assertThat
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TIMEOUT = 1000L
+
+@ConnectivityModuleTest
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class HeaderCompressionUtilsTest {
+
+    private fun decompressHex(hex: String): ByteArray {
+        val bytes = HexDump.hexStringToByteArray(hex)
+        val buf = bytes.copyOf(1500)
+        val newLen = HeaderCompressionUtils.decompress6lowpan(buf, bytes.size)
+        return buf.copyOf(newLen)
+    }
+
+    private fun compressHex(hex: String): ByteArray {
+        val buf = HexDump.hexStringToByteArray(hex)
+        val newLen = HeaderCompressionUtils.compress6lowpan(buf, buf.size)
+        return buf.copyOf(newLen)
+    }
+
+    private fun String.decodeHex() = HexDump.hexStringToByteArray(this)
+
+    @Test
+    fun testHeaderDecompression() {
+        // TF: 00, NH: 0, HLIM: 00, CID: 0, SAC: 0, SAM: 00, M: 0, DAC: 0, DAM: 00
+        var input = "6000" +
+                    "ccf" +                               // ECN + DSCP + 4-bit Pad (here "f")
+                    "12345" +                             // flow label
+                    "11" +                                // next header
+                    "e7" +                                // hop limit
+                    "abcdef1234567890abcdef1234567890" +  // source
+                    "aaabbbcccdddeeefff00011122233344" +  // dest
+                    "abcd"                                // payload
+
+        var output = "6" +                                // version
+                     "cc" +                               // traffic class
+                     "12345" +                            // flow label
+                     "0002" +                             // payload length
+                     "11" +                               // next header
+                     "e7" +                               // hop limit
+                     "abcdef1234567890abcdef1234567890" + // source
+                     "aaabbbcccdddeeefff00011122233344" + // dest
+                     "abcd"                               // payload
+        assertThat(decompressHex(input)).isEqualTo(output.decodeHex())
+
+        // TF: 01, NH: 0, HLIM: 01, CID: 0, SAC: 0, SAM: 01, M: 0, DAC: 0, DAM: 01
+        input  = "6911" +
+                 "5" +                                // ECN + 2-bit pad (here "1")
+                 "f100e" +                            // flow label
+                 "42" +                               // next header
+                 "1102030405060708" +                 // source
+                 "aa0b0c0d0e0f1011" +                 // dest
+                 "abcd"                               // payload
+
+        output = "6" +                                // version
+                 "01" +                               // traffic class
+                 "f100e" +                            // flow label
+                 "0002" +                             // payload length
+                 "42" +                               // next header
+                 "01" +                               // hop limit
+                 "fe800000000000001102030405060708" + // source
+                 "fe80000000000000aa0b0c0d0e0f1011" + // dest
+                 "abcd"                               // payload
+        assertThat(decompressHex(input)).isEqualTo(output.decodeHex())
+
+        // TF: 10, NH: 0, HLIM: 10, CID: 0, SAC: 0, SAM: 10, M: 0, DAC: 0, DAM: 10
+        input  = "7222" +
+                 "cc" +                               // traffic class
+                 "43" +                               // next header
+                 "1234" +                             // source
+                 "abcd" +                             // dest
+                 "abcdef"                             // payload
+
+        output = "6" +                                // version
+                 "cc" +                               // traffic class
+                 "00000" +                            // flow label
+                 "0003" +                             // payload length
+                 "43" +                               // next header
+                 "40" +                               // hop limit
+                 "fe80000000000000000000fffe001234" + // source
+                 "fe80000000000000000000fffe00abcd" + // dest
+                 "abcdef"                             // payload
+        assertThat(decompressHex(input)).isEqualTo(output.decodeHex())
+
+        // TF: 11, NH: 0, HLIM: 11, CID: 0, SAC: 0, SAM: 10, M: 1, DAC: 0, DAM: 00
+        input  = "7b28" +
+                 "44" +                               // next header
+                 "1234" +                             // source
+                 "ff020000000000000000000000000001" + // dest
+                 "abcdef01"                           // payload
+
+        output = "6" +                                // version
+                 "00" +                               // traffic class
+                 "00000" +                            // flow label
+                 "0004" +                             // payload length
+                 "44" +                               // next header
+                 "ff" +                               // hop limit
+                 "fe80000000000000000000fffe001234" + // source
+                 "ff020000000000000000000000000001" + // dest
+                 "abcdef01"                           // payload
+        assertThat(decompressHex(input)).isEqualTo(output.decodeHex())
+
+        // TF: 11, NH: 0, HLIM: 11, CID: 0, SAC: 0, SAM: 10, M: 1, DAC: 0, DAM: 01
+        input  = "7b29" +
+                 "44" +                               // next header
+                 "1234" +                             // source
+                 "02abcdef1234" +                     // dest
+                 "abcdef01"                           // payload
+
+        output = "6" +                                // version
+                 "00" +                               // traffic class
+                 "00000" +                            // flow label
+                 "0004" +                             // payload length
+                 "44" +                               // next header
+                 "ff" +                               // hop limit
+                 "fe80000000000000000000fffe001234" + // source
+                 "ff02000000000000000000abcdef1234" + // dest
+                 "abcdef01"                           // payload
+        assertThat(decompressHex(input)).isEqualTo(output.decodeHex())
+
+        // TF: 11, NH: 0, HLIM: 11, CID: 0, SAC: 0, SAM: 10, M: 1, DAC: 0, DAM: 10
+        input  = "7b2a" +
+                 "44" +                               // next header
+                 "1234" +                             // source
+                 "ee123456" +                         // dest
+                 "abcdef01"                           // payload
+
+        output = "6" +                                // version
+                 "00" +                               // traffic class
+                 "00000" +                            // flow label
+                 "0004" +                             // payload length
+                 "44" +                               // next header
+                 "ff" +                               // hop limit
+                 "fe80000000000000000000fffe001234" + // source
+                 "ffee0000000000000000000000123456" + // dest
+                 "abcdef01"                           // payload
+        assertThat(decompressHex(input)).isEqualTo(output.decodeHex())
+
+        // TF: 11, NH: 0, HLIM: 11, CID: 0, SAC: 0, SAM: 10, M: 1, DAC: 0, DAM: 11
+        input  = "7b2b" +
+                 "44" +                               // next header
+                 "1234" +                             // source
+                 "89" +                               // dest
+                 "abcdef01"                           // payload
+
+        output = "6" +                                // version
+                 "00" +                               // traffic class
+                 "00000" +                            // flow label
+                 "0004" +                             // payload length
+                 "44" +                               // next header
+                 "ff" +                               // hop limit
+                 "fe80000000000000000000fffe001234" + // source
+                 "ff020000000000000000000000000089" + // dest
+                 "abcdef01"                           // payload
+        assertThat(decompressHex(input)).isEqualTo(output.decodeHex())
+    }
+
+    @Test
+    fun testHeaderCompression() {
+        val input  = "60120304000011fffe800000000000000000000000000001fe800000000000000000000000000002"
+        val output = "60000102030411fffe800000000000000000000000000001fe800000000000000000000000000002"
+        assertThat(compressHex(input)).isEqualTo(output.decodeHex())
+    }
+}
diff --git a/tests/unit/java/com/android/server/net/L2capPacketForwarderTest.kt b/tests/unit/java/com/android/server/net/L2capPacketForwarderTest.kt
new file mode 100644
index 0000000..e261732
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/L2capPacketForwarderTest.kt
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net
+
+import android.bluetooth.BluetoothSocket
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.ParcelFileDescriptor
+import android.system.Os
+import android.system.OsConstants.AF_UNIX
+import android.system.OsConstants.SHUT_RD
+import android.system.OsConstants.SHUT_WR
+import android.system.OsConstants.SOCK_SEQPACKET
+import android.system.OsConstants.SOL_SOCKET
+import android.system.OsConstants.SO_RCVTIMEO
+import android.system.OsConstants.SO_SNDTIMEO
+import android.system.StructTimeval
+import com.android.server.net.L2capPacketForwarder.BluetoothSocketWrapper
+import com.android.server.net.L2capPacketForwarder.FdWrapper
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.google.common.truth.Truth.assertThat
+import java.io.FileDescriptor
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+import java.nio.ByteBuffer
+import kotlin.arrayOf
+import kotlin.random.Random
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+private const val TIMEOUT = 1000L
+
+@ConnectivityModuleTest
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class L2capPacketForwarderTest {
+    private lateinit var forwarder: L2capPacketForwarder
+    private val tunFds = arrayOf(FileDescriptor(), FileDescriptor())
+    private val l2capFds = arrayOf(FileDescriptor(), FileDescriptor())
+    private lateinit var l2capInputStream: BluetoothL2capInputStream
+    private lateinit var l2capOutputStream: BluetoothL2capOutputStream
+    @Mock private lateinit var bluetoothSocket: BluetoothSocket
+    @Mock private lateinit var callback: L2capPacketForwarder.ICallback
+
+    private val handlerThread = HandlerThread("L2capPacketForwarderTest thread").apply { start() }
+    private val handler = Handler(handlerThread.looper)
+
+    /** Imitates the behavior of an L2CAP BluetoothSocket */
+    private class BluetoothL2capInputStream(
+        val fd: FileDescriptor,
+    ) : InputStream() {
+        val l2capBuffer = ByteBuffer.wrap(ByteArray(0xffff)).apply {
+            limit(0)
+        }
+
+        override fun read(): Int {
+            throw NotImplementedError("b/391623333: not implemented correctly for L2cap sockets")
+        }
+
+        /** See BluetoothSocket#read(buf, off, len) */
+        override fun read(b: ByteArray, off: Int, len: Int): Int {
+            // If no more bytes are remaining, read from the fd into the intermediate buffer.
+            if (l2capBuffer.remaining() == 0) {
+                // fillL2capRxBuffer()
+                // refill buffer and return - 1
+                val backingArray = l2capBuffer.array()
+                var bytesRead = 0
+                try {
+                    bytesRead = Os.read(fd, backingArray, 0 /*off*/, backingArray.size)
+                } catch (e: Exception) {
+                    // read failed, timed out, or was interrupted
+                    // InputStream throws IOException
+                    throw IOException(e)
+                }
+                l2capBuffer.rewind()
+                l2capBuffer.limit(bytesRead)
+            }
+
+            val bytesToRead = if (len > l2capBuffer.remaining()) l2capBuffer.remaining() else len
+            l2capBuffer.get(b, off, bytesToRead)
+            return bytesToRead
+        }
+
+        override fun available(): Int {
+            throw NotImplementedError("b/391623333: not implemented correctly for L2cap sockets")
+        }
+
+        override fun close() {
+            try {
+                Os.shutdown(fd, SHUT_RD)
+            } catch (e: Exception) {
+                // InputStream throws IOException
+                throw IOException(e)
+            }
+        }
+    }
+
+    /** Imitates the behavior of an L2CAP BluetoothSocket */
+    private class BluetoothL2capOutputStream(
+        val fd: FileDescriptor,
+    ) : OutputStream() {
+
+        override fun write(b: Int) {
+            throw NotImplementedError("This method does not maintain packet boundaries, do not use")
+        }
+
+        /** See BluetoothSocket#write(buf, off, len) */
+        override fun write(b: ByteArray, off: Int, len: Int) {
+            try {
+                Os.write(fd, b, off, len)
+            } catch (e: Exception) {
+                // OutputStream throws IOException
+                throw IOException(e)
+            }
+        }
+
+        override fun close() {
+            try {
+                Os.shutdown(fd, SHUT_WR)
+            } catch (e: Exception) {
+                // OutputStream throws IOException
+                throw IOException(e)
+            }
+        }
+    }
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        Os.socketpair(AF_UNIX, SOCK_SEQPACKET, 0, tunFds[0], tunFds[1])
+        Os.socketpair(AF_UNIX, SOCK_SEQPACKET, 0, l2capFds[0], l2capFds[1])
+
+        // Set socket i/o timeout for test end.
+        Os.setsockoptTimeval(tunFds[1], SOL_SOCKET, SO_RCVTIMEO, StructTimeval.fromMillis(5000))
+        Os.setsockoptTimeval(tunFds[1], SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(5000))
+        Os.setsockoptTimeval(l2capFds[1], SOL_SOCKET, SO_RCVTIMEO, StructTimeval.fromMillis(5000))
+        Os.setsockoptTimeval(l2capFds[1], SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(5000))
+
+        l2capInputStream = BluetoothL2capInputStream(l2capFds[0])
+        l2capOutputStream = BluetoothL2capOutputStream(l2capFds[0])
+        doReturn(l2capInputStream).`when`(bluetoothSocket).getInputStream()
+        doReturn(l2capOutputStream).`when`(bluetoothSocket).getOutputStream()
+        doAnswer({
+            l2capInputStream.close()
+            l2capOutputStream.close()
+            try {
+                // libcore's Linux_close properly invalidates the FileDescriptor, so it is safe to
+                // close multiple times.
+                Os.close(l2capFds[0])
+            } catch (e: Exception) {
+                // BluetoothSocket#close can be called multiple times. Catch EBADF on subsequent
+                // invocations.
+            }
+        }).`when`(bluetoothSocket).close()
+
+        forwarder = L2capPacketForwarder(
+                handler,
+                FdWrapper(ParcelFileDescriptor(tunFds[0])),
+                BluetoothSocketWrapper(bluetoothSocket),
+                false /* compressHeaders */,
+                callback
+        )
+    }
+
+    @After
+    fun tearDown() {
+        if (::forwarder.isInitialized) {
+            // forwarder closes tunFds[0] and l2capFds[0]
+            forwarder.tearDown()
+        } else {
+            Os.close(tunFds[0])
+            Os.close(l2capFds[0])
+        }
+        Os.close(tunFds[1])
+        Os.close(l2capFds[1])
+
+        handlerThread.quitSafely()
+        handlerThread.join()
+    }
+
+    fun sendPacket(fd: FileDescriptor, size: Int = 1280): ByteArray {
+        val packet = ByteArray(size)
+        Random.nextBytes(packet)
+        Os.write(fd, packet, 0 /*off*/, packet.size)
+        return packet
+    }
+
+    fun assertPacketReceived(fd: FileDescriptor, expected: ByteArray) {
+        val readBuffer = ByteArray(expected.size)
+        Os.read(fd, readBuffer, 0 /*off*/, readBuffer.size)
+        assertThat(readBuffer).isEqualTo(expected)
+    }
+
+    @Test
+    fun testForwarding_withoutHeaderCompression() {
+        var packet = sendPacket(l2capFds[1])
+        var packet2 = sendPacket(l2capFds[1])
+        assertPacketReceived(tunFds[1], packet)
+        assertPacketReceived(tunFds[1], packet2)
+
+        packet = sendPacket(tunFds[1])
+        packet2 = sendPacket(tunFds[1])
+        assertPacketReceived(l2capFds[1], packet)
+        assertPacketReceived(l2capFds[1], packet2)
+    }
+
+    @Test
+    fun testForwarding_packetExceedsMtu() {
+        // Reading from tun drops packets that exceed MTU.
+        // drop
+        sendPacket(tunFds[1], L2capPacketForwarder.MTU + 1)
+        // drop
+        sendPacket(tunFds[1], L2capPacketForwarder.MTU + 42)
+        var packet = sendPacket(l2capFds[1], 1280)
+        assertPacketReceived(tunFds[1], packet)
+
+        // On the BluetoothSocket side, reads that exceed MTU stop forwarding.
+        sendPacket(l2capFds[1], L2capPacketForwarder.MTU + 1)
+        verify(callback, timeout(TIMEOUT)).onError()
+    }
+}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index b528480..697bf9e 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -163,12 +163,13 @@
 import com.android.net.module.util.BpfDump;
 import com.android.net.module.util.IBpfMap;
 import com.android.net.module.util.LocationPermissionChecker;
+import com.android.net.module.util.SkDestroyListener;
 import com.android.net.module.util.Struct;
 import com.android.net.module.util.Struct.S32;
 import com.android.net.module.util.Struct.U8;
 import com.android.net.module.util.bpf.CookieTagMapKey;
 import com.android.net.module.util.bpf.CookieTagMapValue;
-import com.android.server.BpfNetMaps;
+import com.android.net.module.util.netlink.InetDiagMessage;
 import com.android.server.connectivity.ConnectivityResources;
 import com.android.server.net.NetworkStatsService.AlertObserver;
 import com.android.server.net.NetworkStatsService.NetworkStatsSettings;
@@ -211,6 +212,7 @@
 import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
 
 /**
  * Tests for {@link NetworkStatsService}.
@@ -286,8 +288,6 @@
     private LocationPermissionChecker mLocationPermissionChecker;
     private TestBpfMap<S32, U8> mUidCounterSetMap = spy(new TestBpfMap<>(S32.class, U8.class));
     @Mock
-    private BpfNetMaps mBpfNetMaps;
-    @Mock
     private SkDestroyListener mSkDestroyListener;
 
     private TestBpfMap<CookieTagMapKey, CookieTagMapValue> mCookieTagMap = new TestBpfMap<>(
@@ -608,13 +608,8 @@
         }
 
         @Override
-        public BpfNetMaps makeBpfNetMaps(Context ctx) {
-            return mBpfNetMaps;
-        }
-
-        @Override
-        public SkDestroyListener makeSkDestroyListener(
-                IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap, Handler handler) {
+        public SkDestroyListener makeSkDestroyListener(Consumer<InetDiagMessage> consumer,
+                Handler handler) {
             return mSkDestroyListener;
         }
 
diff --git a/tests/unit/jni/Android.bp b/tests/unit/jni/Android.bp
index 50971e7..1e9db03 100644
--- a/tests/unit/jni/Android.bp
+++ b/tests/unit/jni/Android.bp
@@ -22,6 +22,7 @@
     ],
 
     shared_libs: [
+        "libbase",
         "liblog",
         "libnativehelper",
         "libnetdutils",
@@ -42,7 +43,7 @@
     ],
     static_libs: [
         "libnet_utils_device_common_bpfjni",
-        "libnet_utils_device_common_timerfdjni",
+        "libserviceconnectivityjni",
         "libtcutils",
     ],
     shared_libs: [
diff --git a/tests/unit/jni/android_net_frameworktests_util/onload.cpp b/tests/unit/jni/android_net_frameworktests_util/onload.cpp
index a0ce4f8..f70b04b 100644
--- a/tests/unit/jni/android_net_frameworktests_util/onload.cpp
+++ b/tests/unit/jni/android_net_frameworktests_util/onload.cpp
@@ -24,7 +24,7 @@
 
 int register_com_android_net_module_util_BpfMap(JNIEnv* env, char const* class_name);
 int register_com_android_net_module_util_TcUtils(JNIEnv* env, char const* class_name);
-int register_com_android_net_module_util_TimerFdUtils(JNIEnv *env,
+int register_com_android_net_module_util_ServiceConnectivityJni(JNIEnv *env,
                                                       char const *class_name);
 
 extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
@@ -40,8 +40,8 @@
     if (register_com_android_net_module_util_TcUtils(env,
             "android/net/frameworktests/util/TcUtils") < 0) return JNI_ERR;
 
-    if (register_com_android_net_module_util_TimerFdUtils(
-            env, "android/net/frameworktests/util/TimerFdUtils") < 0)
+    if (register_com_android_net_module_util_ServiceConnectivityJni(
+            env, "android/net/frameworktests/util/ServiceConnectivityJni") < 0)
       return JNI_ERR;
 
     return JNI_VERSION_1_6;
diff --git a/thread/demoapp/AndroidManifest.xml b/thread/demoapp/AndroidManifest.xml
index c31bb71..fddc151 100644
--- a/thread/demoapp/AndroidManifest.xml
+++ b/thread/demoapp/AndroidManifest.xml
@@ -33,6 +33,7 @@
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
+                <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
             </intent-filter>
         </activity>
     </application>
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 30d5a02..af16d19 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -351,14 +351,14 @@
         }
 
         otDaemon.initialize(
-                mTunIfController.getTunFd(),
                 shouldEnableThread(),
                 newOtDaemonConfig(mPersistentSettings.getConfiguration()),
+                mTunIfController.getTunFd(),
                 mNsdPublisher,
                 getMeshcopTxtAttributes(mResources.get()),
-                mOtDaemonCallbackProxy,
                 mCountryCodeSupplier.get(),
-                FeatureFlags.isTrelEnabled());
+                FeatureFlags.isTrelEnabled(),
+                mOtDaemonCallbackProxy);
         otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0);
         mOtDaemon = otDaemon;
         mHandler.post(mNat64CidrController::maybeUpdateNat64Cidr);
@@ -623,12 +623,17 @@
         mNat64CidrController.maybeUpdateNat64Cidr();
     }
 
-    private static OtDaemonConfiguration newOtDaemonConfig(
-            @NonNull ThreadConfiguration threadConfig) {
+    private OtDaemonConfiguration newOtDaemonConfig(ThreadConfiguration threadConfig) {
+        int srpServerConfig = R.bool.config_thread_srp_server_wait_for_border_routing_enabled;
+        boolean srpServerWaitEnabled = mResources.get().getBoolean(srpServerConfig);
+        int autoJoinConfig = R.bool.config_thread_border_router_auto_join_enabled;
+        boolean autoJoinEnabled = mResources.get().getBoolean(autoJoinConfig);
         return new OtDaemonConfiguration.Builder()
                 .setBorderRouterEnabled(threadConfig.isBorderRouterEnabled())
                 .setNat64Enabled(threadConfig.isNat64Enabled())
                 .setDhcpv6PdEnabled(threadConfig.isDhcpv6PdEnabled())
+                .setSrpServerWaitForBorderRoutingEnabled(srpServerWaitEnabled)
+                .setBorderRouterAutoJoinEnabled(autoJoinEnabled)
                 .build();
     }
 
diff --git a/thread/tests/cts/AndroidTest.xml b/thread/tests/cts/AndroidTest.xml
index e954d3b..89d2ce5 100644
--- a/thread/tests/cts/AndroidTest.xml
+++ b/thread/tests/cts/AndroidTest.xml
@@ -57,13 +57,4 @@
         <option name="exclude-annotation" value="org.junit.Ignore"/>
     </test>
 
-    <!--
-        This doesn't override a read-only flag, to run the tests locally with `epskc_enabled` flag
-        enabled, set the flag to `is_fixed_read_only: false`. This should be removed after the
-        `epskc_enabled` flag is rolled out.
-    -->
-    <target_preparer class="com.android.tradefed.targetprep.FeatureFlagTargetPreparer">
-        <option name="flag-value"
-                value="thread_network/com.android.net.thread.flags.epskc_enabled=true"/>
-    </target_preparer>
 </configuration>
diff --git a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
index aeeed65..875a4ad 100644
--- a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
+++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
@@ -16,7 +16,6 @@
 
 package android.net.thread;
 
-import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
 import static android.net.InetAddresses.parseNumericAddress;
 import static android.net.thread.utils.IntegrationTestUtils.DEFAULT_DATASET;
 import static android.net.thread.utils.IntegrationTestUtils.buildIcmpv4EchoReply;
@@ -38,7 +37,6 @@
 
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REPLY_TYPE;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
-import static com.android.testutils.TestPermissionUtil.runAsShell;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -56,7 +54,6 @@
 import android.net.RouteInfo;
 import android.net.thread.utils.FullThreadDevice;
 import android.net.thread.utils.InfraNetworkDevice;
-import android.net.thread.utils.IntegrationTestUtils;
 import android.net.thread.utils.OtDaemonController;
 import android.net.thread.utils.TestTunNetworkUtils;
 import android.net.thread.utils.ThreadFeatureCheckerRule;
diff --git a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
index d41550b..7a5895f 100644
--- a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
+++ b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
@@ -130,6 +130,7 @@
     public void setUp() throws Exception {
         mExecutor = Executors.newSingleThreadExecutor();
         mOtCtl = new OtDaemonController();
+        mController.setEnabledAndWait(true);
         mController.leaveAndWait();
 
         // TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
index 07d0390..801e21e 100644
--- a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
@@ -39,6 +39,7 @@
 import android.os.SystemClock
 import android.system.OsConstants
 import android.system.OsConstants.IPPROTO_ICMP
+import android.util.Log
 import androidx.test.core.app.ApplicationProvider
 import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
 import com.android.net.module.util.IpUtils
@@ -84,6 +85,8 @@
 
 /** Utilities for Thread integration tests. */
 object IntegrationTestUtils {
+    private val TAG = IntegrationTestUtils::class.simpleName
+
     // The timeout of join() after restarting ot-daemon. The device needs to send 6 Link Request
     // every 5 seconds, followed by 4 Parent Request every second. So this value needs to be 40
     // seconds to be safe
@@ -388,7 +391,12 @@
         raMsg ?: return pioList
 
         val buf = ByteBuffer.wrap(raMsg)
-        val ipv6Header = Struct.parse(Ipv6Header::class.java, buf)
+        val ipv6Header = try {
+            Struct.parse(Ipv6Header::class.java, buf)
+        } catch (e: IllegalArgumentException) {
+            // the packet is not IPv6
+            return pioList
+        }
         if (ipv6Header.nextHeader != OsConstants.IPPROTO_ICMPV6.toByte()) {
             return pioList
         }
@@ -478,6 +486,7 @@
         val serviceInfoFuture = CompletableFuture<NsdServiceInfo>()
         val listener: NsdManager.DiscoveryListener = object : DefaultDiscoveryListener() {
             override fun onServiceFound(serviceInfo: NsdServiceInfo) {
+                Log.d(TAG, "onServiceFound: $serviceInfo")
                 serviceInfoFuture.complete(serviceInfo)
             }
         }
@@ -525,6 +534,7 @@
         val resolvedServiceInfoFuture = CompletableFuture<NsdServiceInfo>()
         val callback: NsdManager.ServiceInfoCallback = object : DefaultServiceInfoCallback() {
             override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {
+                Log.d(TAG, "onServiceUpdated: $serviceInfo")
                 if (predicate.test(serviceInfo)) {
                     resolvedServiceInfoFuture.complete(serviceInfo)
                 }
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 dcbb3f5..bc8da8b 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -231,6 +231,11 @@
 
         when(mConnectivityResources.get()).thenReturn(mResources);
         when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(true);
+        when(mResources.getBoolean(
+                        eq(R.bool.config_thread_srp_server_wait_for_border_routing_enabled)))
+                .thenReturn(true);
+        when(mResources.getBoolean(eq(R.bool.config_thread_border_router_auto_join_enabled)))
+                .thenReturn(true);
         when(mResources.getString(eq(R.string.config_thread_vendor_name)))
                 .thenReturn(TEST_VENDOR_NAME);
         when(mResources.getString(eq(R.string.config_thread_vendor_oui)))
@@ -285,6 +290,11 @@
 
     @Test
     public void initialize_resourceOverlayValuesAreSetToOtDaemon() throws Exception {
+        when(mResources.getBoolean(
+                        eq(R.bool.config_thread_srp_server_wait_for_border_routing_enabled)))
+                .thenReturn(false);
+        when(mResources.getBoolean(eq(R.bool.config_thread_border_router_auto_join_enabled)))
+                .thenReturn(false);
         when(mResources.getString(eq(R.string.config_thread_vendor_name)))
                 .thenReturn(TEST_VENDOR_NAME);
         when(mResources.getString(eq(R.string.config_thread_vendor_oui)))
@@ -297,6 +307,8 @@
         mService.initialize();
         mTestLooper.dispatchAll();
 
+        assertThat(mFakeOtDaemon.getConfiguration().srpServerWaitForBorderRoutingEnabled).isFalse();
+        assertThat(mFakeOtDaemon.getConfiguration().borderRouterAutoJoinEnabled).isFalse();
         MeshcopTxtAttributes meshcopTxts = mFakeOtDaemon.getOverriddenMeshcopTxtAttributes();
         assertThat(meshcopTxts.vendorName).isEqualTo(TEST_VENDOR_NAME);
         assertThat(meshcopTxts.vendorOui).isEqualTo(TEST_VENDOR_OUI_BYTES);