Merge "Stop using adoptShellPermissionIdentity in setUp"
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 28edc8a..f3d6aee 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -31,12 +31,12 @@
         "apishim/**/*.java",
         "src/**/*.java",
         ":framework-connectivity-shared-srcs",
-        ":tethering-module-utils-srcs",
         ":services-tethering-shared-srcs",
         ":statslog-tethering-java-gen",
     ],
     static_libs: [
         "androidx.annotation_annotation",
+        "connectivity-net-module-utils-bpf",
         "modules-utils-build",
         "modules-utils-statemachine",
         "networkstack-client",
@@ -46,6 +46,7 @@
         "net-utils-framework-common",
         "net-utils-device-common",
         "net-utils-device-common-bpf",
+        "net-utils-device-common-ip",
         "net-utils-device-common-netlink",
         "netd-client",
         "tetheringstatsprotos",
@@ -210,8 +211,11 @@
 
 sdk {
     name: "tethering-module-sdk",
-    bootclasspath_fragments: ["com.android.tethering-bootclasspath-fragment"],
-    systemserverclasspath_fragments: ["com.android.tethering-systemserverclasspath-fragment"],
+    apexes: [
+        // Adds exportable dependencies of the APEX to the sdk,
+        // e.g. *classpath_fragments.
+        "com.android.tethering",
+    ],
 }
 
 java_library_static {
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index b3cae7c..a7028b7 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -18,6 +18,20 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+prebuilt_etc {
+    name: "TetheringInProcessFlag",
+    src: "in-process",
+    filename_from_src: true,
+    sub_dir: "flag",
+}
+
+prebuilt_etc {
+    name: "TetheringOutOfProcessFlag",
+    src: "out-of-process",
+    filename_from_src: true,
+    sub_dir: "flag",
+}
+
 // Defaults to enable/disable java targets which uses development APIs. "enabled" may have a
 // different value depending on the branch.
 java_defaults {
@@ -71,10 +85,12 @@
     bpfs: [
         "block.o",
         "clatd.o",
-        "dscp_policy.o",
+        "dscpPolicy.o",
         "netd.o",
         "offload.o",
+        "offload@btf.o",
         "test.o",
+        "test@btf.o",
     ],
     apps: [
         "ServiceConnectivityResources",
@@ -82,6 +98,7 @@
     prebuilts: [
         "current_sdkinfo",
         "privapp_allowlist_com.android.tethering",
+        "TetheringOutOfProcessFlag",
     ],
     manifest: "manifest.json",
     key: "com.android.tethering.key",
@@ -105,6 +122,12 @@
     certificate: "com.android.tethering",
 }
 
+filegroup {
+    name: "connectivity-hiddenapi-files",
+    srcs: ["hiddenapi/*.txt"],
+    visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
+
 // Encapsulate the contributions made by the com.android.tethering to the bootclasspath.
 bootclasspath_fragment {
     name: "com.android.tethering-bootclasspath-fragment",
@@ -183,8 +206,21 @@
     base: "com.android.tethering",
     package_name: "com.android.tethering.inprocess",
     enabled: enable_tethering_next_apex,
+    bpfs: [
+        "block.o",
+        "clatd.o",
+        "dscpPolicy.o",
+        "netd.o",
+        "offload@inprocess.o",
+        "test@inprocess.o",
+    ],
     apps: [
         "ServiceConnectivityResources",
         "InProcessTethering",
     ],
+    prebuilts: [
+        "current_sdkinfo",
+        "privapp_allowlist_com.android.tethering",
+        "TetheringInProcessFlag",
+    ],
 }
diff --git a/Tethering/apex/in-process b/Tethering/apex/in-process
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/Tethering/apex/in-process
diff --git a/Tethering/apex/manifest.json b/Tethering/apex/manifest.json
index 3cb03ed..5d5ede6 100644
--- a/Tethering/apex/manifest.json
+++ b/Tethering/apex/manifest.json
@@ -1,4 +1,7 @@
 {
   "name": "com.android.tethering",
-  "version": 339990000
+
+  // Placeholder module version to be replaced during build.
+  // Do not change!
+  "version": 0
 }
diff --git a/Tethering/apex/out-of-process b/Tethering/apex/out-of-process
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/Tethering/apex/out-of-process
diff --git a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
index b865a8e..18ef631 100644
--- a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
@@ -19,7 +19,6 @@
 import android.net.INetd;
 import android.net.MacAddress;
 import android.net.TetherStatsParcel;
-import android.net.util.SharedLog;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
 import android.util.SparseArray;
@@ -28,6 +27,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.net.module.util.IBpfMap.ThrowingBiConsumer;
+import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.bpf.Tether4Key;
 import com.android.net.module.util.bpf.Tether4Value;
 import com.android.net.module.util.bpf.TetherStatsValue;
diff --git a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
index 0683e5e..fd9dab5 100644
--- a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
@@ -19,7 +19,6 @@
 import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED;
 
 import android.net.MacAddress;
-import android.net.util.SharedLog;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.system.OsConstants;
@@ -31,6 +30,7 @@
 
 import com.android.net.module.util.BpfMap;
 import com.android.net.module.util.IBpfMap.ThrowingBiConsumer;
+import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.bpf.Tether4Key;
 import com.android.net.module.util.bpf.Tether4Value;
 import com.android.net.module.util.bpf.TetherStatsKey;
diff --git a/Tethering/common/TetheringLib/src/android/net/ITetheringEventCallback.aidl b/Tethering/common/TetheringLib/src/android/net/ITetheringEventCallback.aidl
index 836761f..b4e3ba4 100644
--- a/Tethering/common/TetheringLib/src/android/net/ITetheringEventCallback.aidl
+++ b/Tethering/common/TetheringLib/src/android/net/ITetheringEventCallback.aidl
@@ -36,5 +36,4 @@
     void onTetherStatesChanged(in TetherStatesParcel states);
     void onTetherClientsChanged(in List<TetheredClient> clients);
     void onOffloadStatusChanged(int status);
-    void onSupportedTetheringTypes(long supportedBitmap);
 }
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringCallbackStartedParcel.aidl b/Tethering/common/TetheringLib/src/android/net/TetheringCallbackStartedParcel.aidl
index f33f846..253eacb 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringCallbackStartedParcel.aidl
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringCallbackStartedParcel.aidl
@@ -26,7 +26,7 @@
  * @hide
  */
 parcelable TetheringCallbackStartedParcel {
-    long supportedTypes;
+    boolean tetheringSupported;
     Network upstreamNetwork;
     TetheringConfigurationParcel config;
     TetherStatesParcel states;
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index b3f0cf2..6f9b33e 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -183,12 +183,6 @@
      */
     public static final int TETHERING_WIGIG = 6;
 
-    /**
-     * The int value of last tethering type.
-     * @hide
-     */
-    public static final int MAX_TETHERING_TYPE = TETHERING_WIGIG;
-
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(value = {
@@ -526,9 +520,6 @@
         }
 
         @Override
-        public void onSupportedTetheringTypes(long supportedBitmap) { }
-
-        @Override
         public void onUpstreamChanged(Network network) { }
 
         @Override
@@ -1042,29 +1033,15 @@
         /**
          * Called when tethering supported status changed.
          *
-         * <p>This callback will be called immediately after the callback is
-         * registered, and never be called if there is changes afterward.
-         *
-         * <p>Tethering may be disabled via system properties, device configuration, or device
-         * policy restrictions.
-         *
-         * @param supported whether any tethering type is supported.
-         */
-        default void onTetheringSupported(boolean supported) {}
-
-        /**
-         * Called when tethering supported status changed.
-         *
          * <p>This will be called immediately after the callback is registered, and may be called
          * multiple times later upon changes.
          *
          * <p>Tethering may be disabled via system properties, device configuration, or device
          * policy restrictions.
          *
-         * @param supportedTypes a set of @TetheringType which is supported.
-         * @hide
+         * @param supported The new supported status
          */
-        default void onSupportedTetheringTypes(@NonNull Set<Integer> supportedTypes) {}
+        default void onTetheringSupported(boolean supported) {}
 
         /**
          * Called when tethering upstream changed.
@@ -1362,8 +1339,7 @@
                 @Override
                 public void onCallbackStarted(TetheringCallbackStartedParcel parcel) {
                     executor.execute(() -> {
-                        callback.onSupportedTetheringTypes(unpackBits(parcel.supportedTypes));
-                        callback.onTetheringSupported(parcel.supportedTypes != 0);
+                        callback.onTetheringSupported(parcel.tetheringSupported);
                         callback.onUpstreamChanged(parcel.upstreamNetwork);
                         sendErrorCallbacks(parcel.states);
                         sendRegexpsChanged(parcel.config);
@@ -1382,13 +1358,6 @@
                     });
                 }
 
-                @Override
-                public void onSupportedTetheringTypes(long supportedBitmap) {
-                    executor.execute(() -> {
-                        callback.onSupportedTetheringTypes(unpackBits(supportedBitmap));
-                    });
-                }
-
                 private void sendRegexpsChanged(TetheringConfigurationParcel parcel) {
                     callback.onTetherableInterfaceRegexpsChanged(new TetheringInterfaceRegexps(
                             parcel.tetherableBluetoothRegexs,
@@ -1427,23 +1396,6 @@
     }
 
     /**
-     * Unpack bitmap to a set of bit position intergers.
-     * @hide
-     */
-    public static ArraySet<Integer> unpackBits(long val) {
-        final ArraySet<Integer> result = new ArraySet<>(Long.bitCount(val));
-        int bitPos = 0;
-        while (val != 0) {
-            if ((val & 1) == 1) result.add(bitPos);
-
-            val = val >>> 1;
-            bitPos++;
-        }
-
-        return result;
-    }
-
-    /**
      * Remove tethering event callback previously registered with
      * {@link #registerTetheringEventCallback}.
      *
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index 437ed71..438b592 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -44,9 +44,7 @@
 import android.net.dhcp.DhcpServingParamsParcelExt;
 import android.net.dhcp.IDhcpEventCallbacks;
 import android.net.dhcp.IDhcpServer;
-import android.net.ip.IpNeighborMonitor.NeighborEvent;
 import android.net.ip.RouterAdvertisementDaemon.RaParams;
-import android.net.util.SharedLog;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
@@ -64,6 +62,10 @@
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.NetdUtils;
+import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.ip.InterfaceController;
+import com.android.net.module.util.ip.IpNeighborMonitor;
+import com.android.net.module.util.ip.IpNeighborMonitor.NeighborEvent;
 import com.android.networkstack.tethering.BpfCoordinator;
 import com.android.networkstack.tethering.BpfCoordinator.ClientInfo;
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
diff --git a/Tethering/src/android/net/ip/NeighborPacketForwarder.java b/Tethering/src/android/net/ip/NeighborPacketForwarder.java
index 723bd63..8384562 100644
--- a/Tethering/src/android/net/ip/NeighborPacketForwarder.java
+++ b/Tethering/src/android/net/ip/NeighborPacketForwarder.java
@@ -23,6 +23,7 @@
 import static android.system.OsConstants.SOCK_DGRAM;
 import static android.system.OsConstants.SOCK_NONBLOCK;
 import static android.system.OsConstants.SOCK_RAW;
+import static android.system.OsConstants.ENODEV;
 
 import android.net.util.SocketUtils;
 import android.os.Handler;
@@ -131,7 +132,13 @@
                                                         ETH_P_IPV6, mListenIfaceParams.index);
             Os.bind(mFd, bindAddress);
         } catch (ErrnoException | SocketException e) {
-            Log.wtf(mTag, "Failed to create  socket", e);
+            // An ENODEV(No such device) will rise if tethering stopped before this function, this
+            // may happen when enable/disable tethering quickly.
+            if (e instanceof ErrnoException && ((ErrnoException) e).errno == ENODEV) {
+                Log.w(mTag, "Failed to create socket because tethered interface is gone", e);
+            } else {
+                Log.wtf(mTag, "Failed to create socket", e);
+            }
             closeSocketQuietly(mFd);
             return null;
         }
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index c403548..49442a6 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -23,11 +23,11 @@
 import static android.net.NetworkStats.TAG_NONE;
 import static android.net.NetworkStats.UID_ALL;
 import static android.net.NetworkStats.UID_TETHERING;
-import static android.net.ip.ConntrackMonitor.ConntrackEvent;
 import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED;
 import static android.system.OsConstants.ETH_P_IP;
 import static android.system.OsConstants.ETH_P_IPV6;
 
+import static com.android.net.module.util.ip.ConntrackMonitor.ConntrackEvent;
 import static com.android.networkstack.tethering.BpfUtils.DOWNSTREAM;
 import static com.android.networkstack.tethering.BpfUtils.UPSTREAM;
 import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS;
@@ -40,18 +40,14 @@
 import android.net.NetworkStats;
 import android.net.NetworkStats.Entry;
 import android.net.TetherOffloadRuleParcel;
-import android.net.ip.ConntrackMonitor;
-import android.net.ip.ConntrackMonitor.ConntrackEventConsumer;
 import android.net.ip.IpServer;
 import android.net.netstats.provider.NetworkStatsProvider;
-import android.net.util.SharedLog;
 import android.os.Handler;
 import android.os.SystemClock;
 import android.system.ErrnoException;
 import android.system.OsConstants;
 import android.text.TextUtils;
 import android.util.ArraySet;
-import android.util.Base64;
 import android.util.Log;
 import android.util.SparseArray;
 
@@ -61,16 +57,20 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.BpfDump;
 import com.android.net.module.util.BpfMap;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.NetworkStackConstants;
+import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.Struct;
 import com.android.net.module.util.Struct.U32;
 import com.android.net.module.util.bpf.Tether4Key;
 import com.android.net.module.util.bpf.Tether4Value;
 import com.android.net.module.util.bpf.TetherStatsKey;
 import com.android.net.module.util.bpf.TetherStatsValue;
+import com.android.net.module.util.ip.ConntrackMonitor;
+import com.android.net.module.util.ip.ConntrackMonitor.ConntrackEventConsumer;
 import com.android.net.module.util.netlink.ConntrackMessage;
 import com.android.net.module.util.netlink.NetlinkConstants;
 import com.android.net.module.util.netlink.NetlinkSocket;
@@ -125,9 +125,6 @@
     private static final String DUMPSYS_RAWMAP_ARG_STATS = "--stats";
     private static final String DUMPSYS_RAWMAP_ARG_UPSTREAM4 = "--upstream4";
 
-    // Using "," as a separator is safe because base64 characters are [0-9a-zA-Z/=+].
-    private static final String DUMP_BASE64_DELIMITER = ",";
-
     /** The names of all the BPF counters defined in bpf_tethering.h. */
     public static final String[] sBpfCounterNames = getBpfCounterNames();
 
@@ -944,6 +941,8 @@
      * be allowed to be accessed on the handler thread.
      */
     public void dump(@NonNull IndentingPrintWriter pw) {
+        // Note that EthernetTetheringTest#isTetherConfigBpfOffloadEnabled relies on
+        // "mIsBpfEnabled" to check tethering config via dumpsys. Beware of the change if any.
         pw.println("mIsBpfEnabled: " + mIsBpfEnabled);
         pw.println("Polling " + (mPollingStarted ? "started" : "not started"));
         pw.println("Stats provider " + (mStatsProvider != null
@@ -1078,18 +1077,6 @@
         }
     }
 
-    private <K extends Struct, V extends Struct> String bpfMapEntryToBase64String(
-            final K key, final V value) {
-        final byte[] keyBytes = key.writeToBytes();
-        final String keyBase64Str = Base64.encodeToString(keyBytes, Base64.DEFAULT)
-                .replace("\n", "");
-        final byte[] valueBytes = value.writeToBytes();
-        final String valueBase64Str = Base64.encodeToString(valueBytes, Base64.DEFAULT)
-                .replace("\n", "");
-
-        return keyBase64Str + DUMP_BASE64_DELIMITER + valueBase64Str;
-    }
-
     private <K extends Struct, V extends Struct> void dumpRawMap(BpfMap<K, V> map,
             IndentingPrintWriter pw) throws ErrnoException {
         if (map == null) {
@@ -1100,14 +1087,20 @@
             pw.println("No entries");
             return;
         }
-        map.forEach((k, v) -> pw.println(bpfMapEntryToBase64String(k, v)));
+        map.forEach((k, v) -> pw.println(BpfDump.toBase64EncodedString(k, v)));
     }
 
     /**
-     * Dump raw BPF map in base64 encoded strings. For test only.
-     * Only allow to dump one map path once.
-     * Format:
+     * Dump raw BPF map into the base64 encoded strings "<base64 key>,<base64 value>".
+     * Allow to dump only one map path once. For test only.
+     *
+     * Usage:
      * $ dumpsys tethering bpfRawMap --<map name>
+     *
+     * Output:
+     * <base64 encoded key #1>,<base64 encoded value #1>
+     * <base64 encoded key #2>,<base64 encoded value #2>
+     * ..
      */
     public void dumpRawMap(@NonNull IndentingPrintWriter pw, @Nullable String[] args) {
         // TODO: consider checking the arg order that <map name> is after "bpfRawMap". Probably
diff --git a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
index adc95ab..784ebd5 100644
--- a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
+++ b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
@@ -43,7 +43,6 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
-import android.net.util.SharedLog;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Parcel;
@@ -55,6 +54,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.SharedLog;
 
 import java.io.PrintWriter;
 import java.util.BitSet;
@@ -296,7 +296,7 @@
      *  4th priority : Checks whether provisioning is required from RRO configuration.
      *
      * @param config
-     * @return integer {@see #TETHERING_PROVISIONING_NOT_REQUIRED,
+     * @return integer See {@link #TETHERING_PROVISIONING_NOT_REQUIRED,
      *                 #TETHERING_PROVISIONING_REQUIRED,
      *                 #TETHERING_PROVISIONING_CARRIER_UNSUPPORT}
      */
diff --git a/Tethering/src/com/android/networkstack/tethering/IPv6TetheringCoordinator.java b/Tethering/src/com/android/networkstack/tethering/IPv6TetheringCoordinator.java
index f3dcaa2..ab3929d 100644
--- a/Tethering/src/com/android/networkstack/tethering/IPv6TetheringCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/IPv6TetheringCoordinator.java
@@ -24,9 +24,10 @@
 import android.net.RouteInfo;
 import android.net.ip.IpServer;
 import android.net.util.NetworkConstants;
-import android.net.util.SharedLog;
 import android.util.Log;
 
+import com.android.net.module.util.SharedLog;
+
 import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadController.java b/Tethering/src/com/android/networkstack/tethering/OffloadController.java
index d60c21d..94684af 100644
--- a/Tethering/src/com/android/networkstack/tethering/OffloadController.java
+++ b/Tethering/src/com/android/networkstack/tethering/OffloadController.java
@@ -43,7 +43,6 @@
 import android.net.NetworkStats.Entry;
 import android.net.RouteInfo;
 import android.net.netstats.provider.NetworkStatsProvider;
-import android.net.util.SharedLog;
 import android.os.Handler;
 import android.provider.Settings;
 import android.system.ErrnoException;
@@ -53,6 +52,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.netlink.ConntrackMessage;
 import com.android.net.module.util.netlink.NetlinkConstants;
 import com.android.net.module.util.netlink.NetlinkSocket;
diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java b/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
index 9da66d8..fbb342d 100644
--- a/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
+++ b/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
@@ -28,7 +28,6 @@
 import android.hardware.tetheroffload.control.V1_0.NetworkProtocol;
 import android.hardware.tetheroffload.control.V1_0.OffloadCallbackEvent;
 import android.hardware.tetheroffload.control.V1_1.ITetheringOffloadCallback;
-import android.net.util.SharedLog;
 import android.net.util.SocketUtils;
 import android.os.Handler;
 import android.os.NativeHandle;
@@ -40,6 +39,7 @@
 import android.util.Pair;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.netlink.NetlinkSocket;
 import com.android.net.module.util.netlink.StructNfGenMsg;
 import com.android.net.module.util.netlink.StructNlMsgHdr;
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index af017f3..89ed620 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -98,8 +98,6 @@
 import android.net.TetheringManager.TetheringRequest;
 import android.net.TetheringRequestParcel;
 import android.net.ip.IpServer;
-import android.net.shared.NetdUtils;
-import android.net.util.SharedLog;
 import android.net.wifi.WifiClient;
 import android.net.wifi.WifiManager;
 import android.net.wifi.p2p.WifiP2pGroup;
@@ -136,6 +134,8 @@
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
 import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.NetdUtils;
+import com.android.net.module.util.SharedLog;
 import com.android.networkstack.apishim.common.BluetoothPanShim;
 import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceCallbackShim;
 import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceRequestShim;
@@ -281,11 +281,6 @@
     private BluetoothPan mBluetoothPan;
     private PanServiceListener mBluetoothPanListener;
     private ArrayList<Pair<Boolean, IIntResultListener>> mPendingPanRequests;
-    // 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
-    // read from binder thread which called TetheringService directly.
-    private volatile long mSupportedTypeBitmap;
 
     public Tethering(TetheringDependencies deps) {
         mLog.mark("Tethering.constructed");
@@ -517,8 +512,6 @@
         mUpstreamNetworkMonitor.setUpstreamConfig(mConfig.chooseUpstreamAutomatically,
                 mConfig.isDunRequired);
         reportConfigurationChanged(mConfig.toStableParcelable());
-
-        updateSupportedDownstreams(mConfig);
     }
 
     private void maybeDunSettingChanged() {
@@ -1406,7 +1399,9 @@
     private void enableIpServing(int tetheringType, String ifname, int ipServingMode,
             boolean isNcm) {
         ensureIpServerStarted(ifname, tetheringType, isNcm);
-        changeInterfaceState(ifname, ipServingMode);
+        if (tether(ifname, ipServingMode) != TETHER_ERROR_NO_ERROR) {
+            Log.e(TAG, "unable start tethering on iface " + ifname);
+        }
     }
 
     private void disableWifiIpServingCommon(int tetheringType, String ifname) {
@@ -1551,31 +1546,30 @@
         }
     }
 
-    private void changeInterfaceState(String ifname, int requestedState) {
-        final int result;
-        switch (requestedState) {
-            case IpServer.STATE_UNAVAILABLE:
-            case IpServer.STATE_AVAILABLE:
-                result = untether(ifname);
-                break;
-            case IpServer.STATE_TETHERED:
-            case IpServer.STATE_LOCAL_ONLY:
-                result = tether(ifname, requestedState);
-                break;
-            default:
-                Log.wtf(TAG, "Unknown interface state: " + requestedState);
-                return;
-        }
-        if (result != TETHER_ERROR_NO_ERROR) {
-            Log.e(TAG, "unable start or stop tethering on iface " + ifname);
-            return;
-        }
-    }
-
     TetheringConfiguration getTetheringConfiguration() {
         return mConfig;
     }
 
+    boolean hasAnySupportedDownstream() {
+        if ((mConfig.tetherableUsbRegexs.length != 0)
+                || (mConfig.tetherableWifiRegexs.length != 0)
+                || (mConfig.tetherableBluetoothRegexs.length != 0)) {
+            return true;
+        }
+
+        // Before T, isTetheringSupported would return true if wifi, usb and bluetooth tethering are
+        // disabled (whole tethering settings would be hidden). This means tethering would also not
+        // support wifi p2p, ethernet tethering and mirrorlink. This is wrong but probably there are
+        // some devices in the field rely on this to disable tethering entirely.
+        if (!SdkLevel.isAtLeastT()) return false;
+
+        return (mConfig.tetherableWifiP2pRegexs.length != 0)
+                || (mConfig.tetherableNcmRegexs.length != 0)
+                || isEthernetSupported();
+    }
+
+    // TODO: using EtherentManager new API to check whether ethernet is supported when the API is
+    // ready to use.
     private boolean isEthernetSupported() {
         return mContext.getSystemService(Context.ETHERNET_SERVICE) != null;
     }
@@ -2365,7 +2359,7 @@
         mHandler.post(() -> {
             mTetheringEventCallbacks.register(callback, new CallbackCookie(hasListPermission));
             final TetheringCallbackStartedParcel parcel = new TetheringCallbackStartedParcel();
-            parcel.supportedTypes = mSupportedTypeBitmap;
+            parcel.tetheringSupported = isTetheringSupported();
             parcel.upstreamNetwork = mTetherUpstream;
             parcel.config = mConfig.toStableParcelable();
             parcel.states =
@@ -2404,22 +2398,6 @@
         });
     }
 
-    private void reportTetheringSupportedChange(final long supportedBitmap) {
-        final int length = mTetheringEventCallbacks.beginBroadcast();
-        try {
-            for (int i = 0; i < length; i++) {
-                try {
-                    mTetheringEventCallbacks.getBroadcastItem(i).onSupportedTetheringTypes(
-                            supportedBitmap);
-                } catch (RemoteException e) {
-                    // Not really very much to do here.
-                }
-            }
-        } finally {
-            mTetheringEventCallbacks.finishBroadcast();
-        }
-    }
-
     private void reportUpstreamChanged(UpstreamNetworkState ns) {
         final int length = mTetheringEventCallbacks.beginBroadcast();
         final Network network = (ns != null) ? ns.network : null;
@@ -2504,56 +2482,18 @@
         }
     }
 
-    private void updateSupportedDownstreams(final TetheringConfiguration config) {
-        final long preSupportedBitmap = mSupportedTypeBitmap;
-
-        if (!isTetheringAllowed() || mEntitlementMgr.isProvisioningNeededButUnavailable()) {
-            mSupportedTypeBitmap = 0;
-        } else {
-            mSupportedTypeBitmap = makeSupportedDownstreams(config);
-        }
-
-        if (preSupportedBitmap != mSupportedTypeBitmap) {
-            reportTetheringSupportedChange(mSupportedTypeBitmap);
-        }
-    }
-
-    private long makeSupportedDownstreams(final TetheringConfiguration config) {
-        long types = 0;
-        if (config.tetherableUsbRegexs.length != 0) types |= (1 << TETHERING_USB);
-
-        if (config.tetherableWifiRegexs.length != 0) types |= (1 << TETHERING_WIFI);
-
-        if (config.tetherableBluetoothRegexs.length != 0) types |= (1 << TETHERING_BLUETOOTH);
-
-        // Before T, isTetheringSupported would return true if wifi, usb and bluetooth tethering are
-        // disabled (whole tethering settings would be hidden). This means tethering would also not
-        // support wifi p2p, ethernet tethering and mirrorlink. This is wrong but probably there are
-        // some devices in the field rely on this to disable tethering entirely.
-        if (!SdkLevel.isAtLeastT() && types == 0) return types;
-
-        if (config.tetherableNcmRegexs.length != 0) types |= (1 << TETHERING_NCM);
-
-        if (config.tetherableWifiP2pRegexs.length != 0) types |= (1 << TETHERING_WIFI_P2P);
-
-        if (isEthernetSupported()) types |= (1 << TETHERING_ETHERNET);
-
-        return types;
-    }
-
     // if ro.tether.denied = true we default to no tethering
     // gservices could set the secure setting to 1 though to enable it on a build where it
     // had previously been turned off.
-    private boolean isTetheringAllowed() {
+    boolean isTetheringSupported() {
         final int defaultVal = mDeps.isTetheringDenied() ? 0 : 1;
         final boolean tetherSupported = Settings.Global.getInt(mContext.getContentResolver(),
                 Settings.Global.TETHER_SUPPORTED, defaultVal) != 0;
-        return tetherSupported
+        final boolean tetherEnabledInSettings = tetherSupported
                 && !mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_TETHERING);
-    }
 
-    boolean isTetheringSupported() {
-        return mSupportedTypeBitmap > 0;
+        return tetherEnabledInSettings && hasAnySupportedDownstream()
+                && !mEntitlementMgr.isProvisioningNeededButUnavailable();
     }
 
     private void dumpBpf(IndentingPrintWriter pw) {
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
index 7c36054..696a970 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
@@ -30,7 +30,6 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.net.TetheringConfigurationParcel;
-import android.net.util.SharedLog;
 import android.os.PersistableBundle;
 import android.provider.DeviceConfig;
 import android.provider.Settings;
@@ -42,6 +41,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.SharedLog;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
index 8e0354d..611d1cf 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -22,7 +22,6 @@
 import android.content.Context;
 import android.net.INetd;
 import android.net.ip.IpServer;
-import android.net.util.SharedLog;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
@@ -32,6 +31,7 @@
 import androidx.annotation.NonNull;
 
 import com.android.internal.util.StateMachine;
+import com.android.net.module.util.SharedLog;
 import com.android.networkstack.apishim.BluetoothPanShimImpl;
 import com.android.networkstack.apishim.common.BluetoothPanShim;
 import com.android.networkstack.tethering.metrics.TetheringMetrics;
diff --git a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
index f8dd673..16c031b 100644
--- a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
+++ b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
@@ -36,7 +36,6 @@
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
-import android.net.util.SharedLog;
 import android.os.Handler;
 import android.util.Log;
 import android.util.SparseIntArray;
@@ -46,6 +45,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.StateMachine;
+import com.android.net.module.util.SharedLog;
 import com.android.networkstack.apishim.ConnectivityManagerShimImpl;
 import com.android.networkstack.apishim.common.ConnectivityManagerShim;
 import com.android.networkstack.tethering.util.PrefixUtils;
diff --git a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
index e25f2ae..d8e631e 100644
--- a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
+++ b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
@@ -69,7 +69,6 @@
 
     /** Update Tethering stats about caller's package name and downstream type. */
     public void createBuilder(final int downstreamType, final String callerPkg) {
-        mBuilderMap.clear();
         NetworkTetheringReported.Builder statsBuilder =
                     NetworkTetheringReported.newBuilder();
         statsBuilder.setDownstreamType(downstreamTypeToEnum(downstreamType))
diff --git a/Tethering/tests/integration/Android.bp b/Tethering/tests/integration/Android.bp
index 31c3df3..ca8d3de 100644
--- a/Tethering/tests/integration/Android.bp
+++ b/Tethering/tests/integration/Android.bp
@@ -32,6 +32,7 @@
         "net-tests-utils",
         "net-utils-device-common-bpf",
         "testables",
+        "connectivity-net-module-utils-bpf",
     ],
     libs: [
         "android.test.runner",
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index 819936d..86dca1c 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -32,6 +32,7 @@
 import static android.system.OsConstants.IPPROTO_IPV6;
 import static android.system.OsConstants.IPPROTO_UDP;
 
+import static com.android.net.module.util.BpfDump.BASE64_DELIMITER;
 import static com.android.net.module.util.ConnectivityUtils.isIPv6ULA;
 import static com.android.net.module.util.HexDump.dumpHexString;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV4;
@@ -156,7 +157,6 @@
     private static final String DUMPSYS_TETHERING_RAWMAP_ARG = "bpfRawMap";
     private static final String DUMPSYS_RAWMAP_ARG_STATS = "--stats";
     private static final String DUMPSYS_RAWMAP_ARG_UPSTREAM4 = "--upstream4";
-    private static final String BASE64_DELIMITER = ",";
     private static final String LINE_DELIMITER = "\\n";
 
     // version=6, traffic class=0x0, flowlabel=0x0;
@@ -192,13 +192,12 @@
         mUiAutomation.adoptShellPermissionIdentity(
                 MANAGE_TEST_NETWORKS, NETWORK_SETTINGS, TETHER_PRIVILEGED, ACCESS_NETWORK_STATE,
                 CONNECTIVITY_USE_RESTRICTED_NETWORKS, DUMP);
+        mRunTests = mTm.isTetheringSupported() && mEm != null;
+        assumeTrue(mRunTests);
+
         mHandlerThread = new HandlerThread(getClass().getSimpleName());
         mHandlerThread.start();
         mHandler = new Handler(mHandlerThread.getLooper());
-
-        mRunTests = isEthernetTetheringSupported();
-        assumeTrue(mRunTests);
-
         mTetheredInterfaceRequester = new TetheredInterfaceRequester(mHandler, mEm);
     }
 
@@ -226,6 +225,7 @@
             mHandler.post(() -> reader.stop());
             mDownstreamReader = null;
         }
+        mHandlerThread.quitSafely();
         mTetheredInterfaceRequester.release();
         mEm.setIncludeTestInterfaces(false);
         maybeDeleteTestInterface();
@@ -236,7 +236,6 @@
         try {
             if (mRunTests) cleanUp();
         } finally {
-            mHandlerThread.quitSafely();
             mUiAutomation.dropShellPermissionIdentity();
         }
     }
@@ -411,23 +410,6 @@
         // client, which is not possible in this test.
     }
 
-    private boolean isEthernetTetheringSupported() throws Exception {
-        final CompletableFuture<Boolean> future = new CompletableFuture<>();
-        final TetheringEventCallback callback = new TetheringEventCallback() {
-            @Override
-            public void onSupportedTetheringTypes(Set<Integer> supportedTypes) {
-                future.complete(supportedTypes.contains(TETHERING_ETHERNET));
-            }
-        };
-
-        try {
-            mTm.registerTetheringEventCallback(mHandler::post, callback);
-            return future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
-        } finally {
-            mTm.unregisterTetheringEventCallback(callback);
-        }
-    }
-
     private static final class MyTetheringEventCallback implements TetheringEventCallback {
         private final TetheringManager mTm;
         private final CountDownLatch mTetheringStartedLatch = new CountDownLatch(1);
@@ -1125,12 +1107,18 @@
     @IgnoreUpTo(Build.VERSION_CODES.R)
     public void testTetherUdpV4AfterR() throws Exception {
         final String kernelVersion = VintfRuntimeInfo.getKernelVersion();
-        boolean usingBpf = isUdpOffloadSupportedByKernel(kernelVersion);
-        if (!usingBpf) {
+        final boolean isUdpOffloadSupported = isUdpOffloadSupportedByKernel(kernelVersion);
+        if (!isUdpOffloadSupported) {
             Log.i(TAG, "testTetherUdpV4AfterR will skip BPF offload test for kernel "
                     + kernelVersion);
         }
-        runUdp4Test(initTetheringTester(toList(TEST_IP4_ADDR), toList(TEST_IP4_DNS)), usingBpf);
+        final boolean isTetherConfigBpfOffloadEnabled = isTetherConfigBpfOffloadEnabled();
+        if (!isTetherConfigBpfOffloadEnabled) {
+            Log.i(TAG, "testTetherUdpV4AfterR will skip BPF offload test "
+                    + "because tethering config doesn't enable BPF offload.");
+        }
+        runUdp4Test(initTetheringTester(toList(TEST_IP4_ADDR), toList(TEST_IP4_DNS)),
+                isUdpOffloadSupported && isTetherConfigBpfOffloadEnabled);
     }
 
     @Nullable
@@ -1189,6 +1177,21 @@
         return null;
     }
 
+    private boolean isTetherConfigBpfOffloadEnabled() throws Exception {
+        final String dumpStr = DumpTestUtils.dumpService(Context.TETHERING_SERVICE, "--short");
+
+        // BPF offload tether config can be overridden by "config_tether_enable_bpf_offload" in
+        // packages/modules/Connectivity/Tethering/res/values/config.xml. OEM may disable config by
+        // RRO to override the enabled default value. Get the tethering config via dumpsys.
+        // $ dumpsys tethering
+        //   mIsBpfEnabled: true
+        boolean enabled = dumpStr.contains("mIsBpfEnabled: true");
+        if (!enabled) {
+            Log.d(TAG, "BPF offload tether config not enabled: " + dumpStr);
+        }
+        return enabled;
+    }
+
     @NonNull
     private Inet6Address getClatIpv6Address(TetheringTester tester, TetheredDevice tethered)
             throws Exception {
diff --git a/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java b/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
index 7ee69b2..d38a7c3 100644
--- a/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
+++ b/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
@@ -28,7 +28,6 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
-import android.net.util.SharedLog;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Looper;
@@ -38,6 +37,7 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.netlink.StructNlMsgHdr;
 
 import org.junit.Before;
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index bf7e887..f242227 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -83,10 +83,7 @@
 import android.net.dhcp.IDhcpEventCallbacks;
 import android.net.dhcp.IDhcpServer;
 import android.net.dhcp.IDhcpServerCallbacks;
-import android.net.ip.IpNeighborMonitor.NeighborEvent;
-import android.net.ip.IpNeighborMonitor.NeighborEventConsumer;
 import android.net.ip.RouterAdvertisementDaemon.RaParams;
-import android.net.util.SharedLog;
 import android.os.Build;
 import android.os.Handler;
 import android.os.RemoteException;
@@ -101,10 +98,15 @@
 import com.android.net.module.util.BpfMap;
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.NetworkStackConstants;
+import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.bpf.Tether4Key;
 import com.android.net.module.util.bpf.Tether4Value;
 import com.android.net.module.util.bpf.TetherStatsKey;
 import com.android.net.module.util.bpf.TetherStatsValue;
+import com.android.net.module.util.ip.ConntrackMonitor;
+import com.android.net.module.util.ip.IpNeighborMonitor;
+import com.android.net.module.util.ip.IpNeighborMonitor.NeighborEvent;
+import com.android.net.module.util.ip.IpNeighborMonitor.NeighborEventConsumer;
 import com.android.networkstack.tethering.BpfCoordinator;
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
 import com.android.networkstack.tethering.PrivateAddressCoordinator;
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
index 3630f24..fa1d881 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -23,7 +23,6 @@
 import static android.net.NetworkStats.TAG_NONE;
 import static android.net.NetworkStats.UID_ALL;
 import static android.net.NetworkStats.UID_TETHERING;
-import static android.net.ip.ConntrackMonitor.ConntrackEvent;
 import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED;
 import static android.system.OsConstants.ETH_P_IP;
 import static android.system.OsConstants.ETH_P_IPV6;
@@ -33,6 +32,7 @@
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.staticMockMarker;
+import static com.android.net.module.util.ip.ConntrackMonitor.ConntrackEvent;
 import static com.android.net.module.util.netlink.ConntrackMessage.DYING_MASK;
 import static com.android.net.module.util.netlink.ConntrackMessage.ESTABLISHED_MASK;
 import static com.android.net.module.util.netlink.ConntrackMessage.Tuple;
@@ -82,10 +82,7 @@
 import android.net.NetworkStats;
 import android.net.TetherOffloadRuleParcel;
 import android.net.TetherStatsParcel;
-import android.net.ip.ConntrackMonitor;
-import android.net.ip.ConntrackMonitor.ConntrackEventConsumer;
 import android.net.ip.IpServer;
-import android.net.util.SharedLog;
 import android.os.Build;
 import android.os.Handler;
 import android.os.test.TestLooper;
@@ -100,10 +97,13 @@
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.NetworkStackConstants;
+import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.bpf.Tether4Key;
 import com.android.net.module.util.bpf.Tether4Value;
 import com.android.net.module.util.bpf.TetherStatsKey;
 import com.android.net.module.util.bpf.TetherStatsValue;
+import com.android.net.module.util.ip.ConntrackMonitor;
+import com.android.net.module.util.ip.ConntrackMonitor.ConntrackEventConsumer;
 import com.android.net.module.util.netlink.ConntrackMessage;
 import com.android.net.module.util.netlink.NetlinkConstants;
 import com.android.net.module.util.netlink.NetlinkSocket;
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 01d7b4b..e4263db 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
@@ -65,7 +65,6 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
-import android.net.util.SharedLog;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.PersistableBundle;
@@ -82,6 +81,7 @@
 
 import com.android.internal.util.test.BroadcastInterceptingContext;
 import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.SharedLog;
 import com.android.testutils.DevSdkIgnoreRule;
 
 import org.junit.After;
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/FakeTetheringConfiguration.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/FakeTetheringConfiguration.java
index ac5c59d..95ec38f 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/FakeTetheringConfiguration.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/FakeTetheringConfiguration.java
@@ -18,7 +18,8 @@
 
 import android.content.Context;
 import android.content.res.Resources;
-import android.net.util.SharedLog;
+
+import com.android.net.module.util.SharedLog;
 
 /** FakeTetheringConfiguration is used to override static method for testing. */
 public class FakeTetheringConfiguration extends TetheringConfiguration {
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/IPv6TetheringCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/IPv6TetheringCoordinatorTest.java
index f2b5314..865228a 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/IPv6TetheringCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/IPv6TetheringCoordinatorTest.java
@@ -41,11 +41,12 @@
 import android.net.NetworkCapabilities;
 import android.net.RouteInfo;
 import android.net.ip.IpServer;
-import android.net.util.SharedLog;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.net.module.util.SharedLog;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java
index 8ef0c76..faca1c8 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java
@@ -67,7 +67,6 @@
 import android.net.NetworkStats.Entry;
 import android.net.RouteInfo;
 import android.net.netstats.provider.NetworkStatsProvider;
-import android.net.util.SharedLog;
 import android.os.Build;
 import android.os.Handler;
 import android.os.test.TestLooper;
@@ -79,6 +78,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.net.module.util.SharedLog;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.TestableNetworkStatsProviderCbBinder;
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java
index d1891ed..36b439b 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java
@@ -43,7 +43,6 @@
 import android.hardware.tetheroffload.control.V1_0.NetworkProtocol;
 import android.hardware.tetheroffload.control.V1_1.ITetheringOffloadCallback;
 import android.hardware.tetheroffload.control.V1_1.OffloadCallbackEvent;
-import android.net.util.SharedLog;
 import android.os.Handler;
 import android.os.NativeHandle;
 import android.os.test.TestLooper;
@@ -55,6 +54,7 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.netlink.StructNfGenMsg;
 import com.android.net.module.util.netlink.StructNlMsgHdr;
 
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
index 3190f35..1a12125 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
@@ -47,7 +47,6 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
-import android.net.util.SharedLog;
 import android.os.Build;
 import android.os.PersistableBundle;
 import android.provider.DeviceConfig;
@@ -63,6 +62,7 @@
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.SharedLog;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
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 773cae3..b402bc3 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -144,7 +144,6 @@
 import android.net.TetheringCallbackStartedParcel;
 import android.net.TetheringConfigurationParcel;
 import android.net.TetheringInterface;
-import android.net.TetheringManager;
 import android.net.TetheringRequestParcel;
 import android.net.dhcp.DhcpLeaseParcelable;
 import android.net.dhcp.DhcpServerCallbacks;
@@ -152,11 +151,9 @@
 import android.net.dhcp.IDhcpEventCallbacks;
 import android.net.dhcp.IDhcpServer;
 import android.net.ip.DadProxy;
-import android.net.ip.IpNeighborMonitor;
 import android.net.ip.IpServer;
 import android.net.ip.RouterAdvertisementDaemon;
 import android.net.util.NetworkConstants;
-import android.net.util.SharedLog;
 import android.net.wifi.SoftApConfiguration;
 import android.net.wifi.WifiClient;
 import android.net.wifi.WifiManager;
@@ -178,7 +175,6 @@
 import android.telephony.PhoneStateListener;
 import android.telephony.TelephonyManager;
 import android.test.mock.MockContentResolver;
-import android.util.ArraySet;
 
 import androidx.annotation.NonNull;
 import androidx.test.filters.SmallTest;
@@ -189,6 +185,8 @@
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.InterfaceParams;
+import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.ip.IpNeighborMonitor;
 import com.android.networkstack.apishim.common.BluetoothPanShim;
 import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceCallbackShim;
 import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceRequestShim;
@@ -219,7 +217,6 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
-import java.util.Set;
 import java.util.Vector;
 
 @RunWith(AndroidJUnit4.class)
@@ -1732,7 +1729,6 @@
         private final ArrayList<TetherStatesParcel> mTetherStates = new ArrayList<>();
         private final ArrayList<Integer> mOffloadStatus = new ArrayList<>();
         private final ArrayList<List<TetheredClient>> mTetheredClients = new ArrayList<>();
-        private final ArrayList<Long> mSupportedBitmaps = new ArrayList<>();
 
         // This function will remove the recorded callbacks, so it must be called once for
         // each callback. If this is called after multiple callback, the order matters.
@@ -1785,10 +1781,6 @@
             assertTrue(leases.containsAll(result));
         }
 
-        public void expectSupportedTetheringTypes(Set<Integer> expectedTypes) {
-            assertEquals(expectedTypes, TetheringManager.unpackBits(mSupportedBitmaps.remove(0)));
-        }
-
         @Override
         public void onUpstreamChanged(Network network) {
             mActualUpstreams.add(network);
@@ -1821,17 +1813,11 @@
             mTetherStates.add(parcel.states);
             mOffloadStatus.add(parcel.offloadStatus);
             mTetheredClients.add(parcel.tetheredClients);
-            mSupportedBitmaps.add(parcel.supportedTypes);
         }
 
         @Override
         public void onCallbackStopped(int errorCode) { }
 
-        @Override
-        public void onSupportedTetheringTypes(long supportedBitmap) {
-            mSupportedBitmaps.add(supportedBitmap);
-        }
-
         public void assertNoUpstreamChangeCallback() {
             assertTrue(mActualUpstreams.isEmpty());
         }
@@ -2959,81 +2945,53 @@
         runStopUSBTethering();
     }
 
-    public static ArraySet<Integer> getAllSupportedTetheringTypes() {
-        return new ArraySet<>(new Integer[] { TETHERING_USB, TETHERING_NCM, TETHERING_WIFI,
-                TETHERING_WIFI_P2P, TETHERING_BLUETOOTH, TETHERING_ETHERNET });
-    }
-
     @Test
     public void testTetheringSupported() throws Exception {
-        final ArraySet<Integer> expectedTypes = getAllSupportedTetheringTypes();
-        // Check tethering is supported after initialization.
         setTetheringSupported(true /* supported */);
-        TestTetheringEventCallback callback = new TestTetheringEventCallback();
-        mTethering.registerTetheringEventCallback(callback);
-        mLooper.dispatchAll();
-        updateConfigAndVerifySupported(callback, expectedTypes);
+        updateConfigAndVerifySupported(true /* supported */);
 
         // Could disable tethering supported by settings.
         Settings.Global.putInt(mContentResolver, Settings.Global.TETHER_SUPPORTED, 0);
-        updateConfigAndVerifySupported(callback, new ArraySet<>());
+        updateConfigAndVerifySupported(false /* supported */);
 
         // Could disable tethering supported by user restriction.
         setTetheringSupported(true /* supported */);
-        updateConfigAndVerifySupported(callback, expectedTypes);
         when(mUserManager.hasUserRestriction(
                 UserManager.DISALLOW_CONFIG_TETHERING)).thenReturn(true);
-        updateConfigAndVerifySupported(callback, new ArraySet<>());
+        updateConfigAndVerifySupported(false /* supported */);
 
         // Tethering is supported if it has any supported downstream.
         setTetheringSupported(true /* supported */);
-        updateConfigAndVerifySupported(callback, expectedTypes);
-        // Usb tethering is not supported:
-        expectedTypes.remove(TETHERING_USB);
         when(mResources.getStringArray(R.array.config_tether_usb_regexs))
                 .thenReturn(new String[0]);
-        updateConfigAndVerifySupported(callback, expectedTypes);
-        // Wifi tethering is not supported:
-        expectedTypes.remove(TETHERING_WIFI);
+        updateConfigAndVerifySupported(true /* supported */);
         when(mResources.getStringArray(R.array.config_tether_wifi_regexs))
                 .thenReturn(new String[0]);
-        updateConfigAndVerifySupported(callback, expectedTypes);
-        // Bluetooth tethering is not supported:
-        expectedTypes.remove(TETHERING_BLUETOOTH);
-        when(mResources.getStringArray(R.array.config_tether_bluetooth_regexs))
-                .thenReturn(new String[0]);
+        updateConfigAndVerifySupported(true /* supported */);
+
 
         if (isAtLeastT()) {
-            updateConfigAndVerifySupported(callback, expectedTypes);
-
-            // P2p tethering is not supported:
-            expectedTypes.remove(TETHERING_WIFI_P2P);
+            when(mResources.getStringArray(R.array.config_tether_bluetooth_regexs))
+                    .thenReturn(new String[0]);
+            updateConfigAndVerifySupported(true /* supported */);
             when(mResources.getStringArray(R.array.config_tether_wifi_p2p_regexs))
                     .thenReturn(new String[0]);
-            updateConfigAndVerifySupported(callback, expectedTypes);
-            // Ncm tethering is not supported:
-            expectedTypes.remove(TETHERING_NCM);
+            updateConfigAndVerifySupported(true /* supported */);
             when(mResources.getStringArray(R.array.config_tether_ncm_regexs))
                     .thenReturn(new String[0]);
-            updateConfigAndVerifySupported(callback, expectedTypes);
-            // Ethernet tethering (last supported type) is not supported:
-            expectedTypes.remove(TETHERING_ETHERNET);
+            updateConfigAndVerifySupported(true /* supported */);
             mForceEthernetServiceUnavailable = true;
-            updateConfigAndVerifySupported(callback, new ArraySet<>());
-
+            updateConfigAndVerifySupported(false /* supported */);
         } else {
-            // If wifi, usb and bluetooth are all not supported, all the types are not supported.
-            expectedTypes.clear();
-            updateConfigAndVerifySupported(callback, expectedTypes);
+            when(mResources.getStringArray(R.array.config_tether_bluetooth_regexs))
+                    .thenReturn(new String[0]);
+            updateConfigAndVerifySupported(false /* supported */);
         }
     }
 
-    private void updateConfigAndVerifySupported(final TestTetheringEventCallback callback,
-            final ArraySet<Integer> expectedTypes) {
+    private void updateConfigAndVerifySupported(boolean supported) {
         sendConfigurationChanged();
-
-        assertEquals(expectedTypes.size() > 0, mTethering.isTetheringSupported());
-        callback.expectSupportedTetheringTypes(expectedTypes);
+        assertEquals(supported, mTethering.isTetheringSupported());
     }
     // TODO: Test that a request for hotspot mode doesn't interfere with an
     // already operating tethering mode interface.
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 97cebd8..9b9507b 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
@@ -49,7 +49,6 @@
 import android.net.LinkProperties;
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
-import android.net.util.SharedLog;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
@@ -60,6 +59,7 @@
 
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
+import com.android.net.module.util.SharedLog;
 import com.android.networkstack.tethering.TestConnectivityManager.NetworkRequestInfo;
 import com.android.networkstack.tethering.TestConnectivityManager.TestNetworkAgent;
 
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
index c34cf5f..6a85718 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
@@ -81,6 +81,22 @@
         mTetheringMetrics = spy(new MockTetheringMetrics());
     }
 
+    private void verifyReport(DownstreamType downstream, ErrorCode error, UserType user)
+            throws Exception {
+        final NetworkTetheringReported expectedReport =
+                mStatsBuilder.setDownstreamType(downstream)
+                .setUserType(user)
+                .setUpstreamType(UpstreamType.UT_UNKNOWN)
+                .setErrorCode(error)
+                .build();
+        verify(mTetheringMetrics).write(expectedReport);
+    }
+
+    private void updateErrorAndSendReport(int downstream, int error) {
+        mTetheringMetrics.updateErrorCode(downstream, error);
+        mTetheringMetrics.sendReport(downstream);
+    }
+
     private void runDownstreamTypesTest(final Pair<Integer, DownstreamType>... testPairs)
             throws Exception {
         for (Pair<Integer, DownstreamType> testPair : testPairs) {
@@ -88,15 +104,8 @@
             final DownstreamType expectedResult = testPair.second;
 
             mTetheringMetrics.createBuilder(type, TEST_CALLER_PKG);
-            mTetheringMetrics.updateErrorCode(type, TETHER_ERROR_NO_ERROR);
-            mTetheringMetrics.sendReport(type);
-            NetworkTetheringReported expectedReport =
-                    mStatsBuilder.setDownstreamType(expectedResult)
-                    .setUserType(UserType.USER_UNKNOWN)
-                    .setUpstreamType(UpstreamType.UT_UNKNOWN)
-                    .setErrorCode(ErrorCode.EC_NO_ERROR)
-                    .build();
-            verify(mTetheringMetrics).write(expectedReport);
+            updateErrorAndSendReport(type, TETHER_ERROR_NO_ERROR);
+            verifyReport(expectedResult, ErrorCode.EC_NO_ERROR, UserType.USER_UNKNOWN);
             reset(mTetheringMetrics);
         }
     }
@@ -118,15 +127,8 @@
             final ErrorCode expectedResult = testPair.second;
 
             mTetheringMetrics.createBuilder(TETHERING_WIFI, TEST_CALLER_PKG);
-            mTetheringMetrics.updateErrorCode(TETHERING_WIFI, errorCode);
-            mTetheringMetrics.sendReport(TETHERING_WIFI);
-            NetworkTetheringReported expectedReport =
-                    mStatsBuilder.setDownstreamType(DownstreamType.DS_TETHERING_WIFI)
-                    .setUserType(UserType.USER_UNKNOWN)
-                    .setUpstreamType(UpstreamType.UT_UNKNOWN)
-                    .setErrorCode(expectedResult)
-                    .build();
-            verify(mTetheringMetrics).write(expectedReport);
+            updateErrorAndSendReport(TETHERING_WIFI, errorCode);
+            verifyReport(DownstreamType.DS_TETHERING_WIFI, expectedResult, UserType.USER_UNKNOWN);
             reset(mTetheringMetrics);
         }
     }
@@ -163,15 +165,8 @@
             final UserType expectedResult = testPair.second;
 
             mTetheringMetrics.createBuilder(TETHERING_WIFI, callerPkg);
-            mTetheringMetrics.updateErrorCode(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
-            mTetheringMetrics.sendReport(TETHERING_WIFI);
-            NetworkTetheringReported expectedReport =
-                    mStatsBuilder.setDownstreamType(DownstreamType.DS_TETHERING_WIFI)
-                    .setUserType(expectedResult)
-                    .setUpstreamType(UpstreamType.UT_UNKNOWN)
-                    .setErrorCode(ErrorCode.EC_NO_ERROR)
-                    .build();
-            verify(mTetheringMetrics).write(expectedReport);
+            updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
+            verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_NO_ERROR, expectedResult);
             reset(mTetheringMetrics);
         }
     }
@@ -183,4 +178,23 @@
                 new Pair<>(SYSTEMUI_PKG, UserType.USER_SYSTEMUI),
                 new Pair<>(GMS_PKG, UserType.USER_GMS));
     }
+
+    @Test
+    public void testMultiBuildersCreatedBeforeSendReport() throws Exception {
+        mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG);
+        mTetheringMetrics.createBuilder(TETHERING_USB, SYSTEMUI_PKG);
+        mTetheringMetrics.createBuilder(TETHERING_BLUETOOTH, GMS_PKG);
+
+        updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_DHCPSERVER_ERROR);
+        verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_DHCPSERVER_ERROR,
+                UserType.USER_SETTINGS);
+
+        updateErrorAndSendReport(TETHERING_USB, TETHER_ERROR_ENABLE_FORWARDING_ERROR);
+        verifyReport(DownstreamType.DS_TETHERING_USB, ErrorCode.EC_ENABLE_FORWARDING_ERROR,
+                UserType.USER_SYSTEMUI);
+
+        updateErrorAndSendReport(TETHERING_BLUETOOTH, TETHER_ERROR_TETHER_IFACE_ERROR);
+        verifyReport(DownstreamType.DS_TETHERING_BLUETOOTH, ErrorCode.EC_TETHER_IFACE_ERROR,
+                UserType.USER_GMS);
+    }
 }
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
index 78fca29..c2e28f4 100644
--- a/bpf_progs/Android.bp
+++ b/bpf_progs/Android.bp
@@ -72,8 +72,8 @@
 }
 
 bpf {
-    name: "dscp_policy.o",
-    srcs: ["dscp_policy.c"],
+    name: "dscpPolicy.o",
+    srcs: ["dscpPolicy.c"],
     btf: true,
     cflags: [
         "-Wall",
@@ -92,6 +92,29 @@
 }
 
 bpf {
+    name: "offload@btf.o",
+    srcs: ["offload@btf.c"],
+    btf: true,
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-DBTF",
+    ],
+}
+
+bpf {
+    name: "offload@inprocess.o",
+    srcs: ["offload@inprocess.c"],
+    btf: true,
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-DBTF",
+        "-DINPROCESS",
+    ],
+}
+
+bpf {
     name: "test.o",
     srcs: ["test.c"],
     cflags: [
@@ -101,6 +124,29 @@
 }
 
 bpf {
+    name: "test@btf.o",
+    srcs: ["test@btf.c"],
+    btf: true,
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-DBTF",
+    ],
+}
+
+bpf {
+    name: "test@inprocess.o",
+    srcs: ["test@inprocess.c"],
+    btf: true,
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-DBTF",
+        "-DINPROCESS",
+    ],
+}
+
+bpf {
     name: "clatd.o",
     srcs: ["clatd.c"],
     btf: true,
diff --git a/bpf_progs/dscpPolicy.c b/bpf_progs/dscpPolicy.c
new file mode 100644
index 0000000..25abd2b
--- /dev/null
+++ b/bpf_progs/dscpPolicy.c
@@ -0,0 +1,319 @@
+/*
+ * 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.
+ */
+
+#include <linux/bpf.h>
+#include <linux/if_ether.h>
+#include <linux/if_packet.h>
+#include <linux/ip.h>
+#include <linux/ipv6.h>
+#include <linux/pkt_cls.h>
+#include <linux/tcp.h>
+#include <linux/types.h>
+#include <netinet/in.h>
+#include <netinet/udp.h>
+#include <stdint.h>
+#include <string.h>
+
+// The resulting .o needs to load on the Android T beta 3 bpfloader
+#define BPFLOADER_MIN_VER BPFLOADER_T_BETA3_VERSION
+
+#include "bpf_helpers.h"
+#include "dscpPolicy.h"
+
+#define ECN_MASK 3
+#define IP4_OFFSET(field, header) (header + offsetof(struct iphdr, field))
+#define UPDATE_TOS(dscp, tos) (dscp << 2) | (tos & ECN_MASK)
+#define UPDATE_PRIORITY(dscp) ((dscp >> 2) + 0x60)
+#define UPDATE_FLOW_LABEL(dscp, flow_lbl) ((dscp & 0xf) << 6) + (flow_lbl >> 6)
+
+DEFINE_BPF_MAP_GRW(switch_comp_map, ARRAY, int, uint64_t, 1, AID_SYSTEM)
+
+DEFINE_BPF_MAP_GRW(ipv4_socket_to_policies_map_A, HASH, uint64_t, RuleEntry, MAX_POLICIES,
+                   AID_SYSTEM)
+DEFINE_BPF_MAP_GRW(ipv4_socket_to_policies_map_B, HASH, uint64_t, RuleEntry, MAX_POLICIES,
+                   AID_SYSTEM)
+DEFINE_BPF_MAP_GRW(ipv6_socket_to_policies_map_A, HASH, uint64_t, RuleEntry, MAX_POLICIES,
+                   AID_SYSTEM)
+DEFINE_BPF_MAP_GRW(ipv6_socket_to_policies_map_B, HASH, uint64_t, RuleEntry, MAX_POLICIES,
+                   AID_SYSTEM)
+
+DEFINE_BPF_MAP_GRW(ipv4_dscp_policies_map, ARRAY, uint32_t, DscpPolicy, MAX_POLICIES, AID_SYSTEM)
+DEFINE_BPF_MAP_GRW(ipv6_dscp_policies_map, ARRAY, uint32_t, DscpPolicy, MAX_POLICIES, AID_SYSTEM)
+
+static inline __always_inline void match_policy(struct __sk_buff* skb, bool ipv4, bool is_eth) {
+    void* data = (void*)(long)skb->data;
+    const void* data_end = (void*)(long)skb->data_end;
+
+    const int l2_header_size = is_eth ? sizeof(struct ethhdr) : 0;
+    struct ethhdr* eth = is_eth ? data : NULL;
+
+    if (data + l2_header_size > data_end) return;
+
+    int zero = 0;
+    int hdr_size = 0;
+    uint64_t* selected_map = bpf_switch_comp_map_lookup_elem(&zero);
+
+    // use this with HASH map so map lookup only happens once policies have been added?
+    if (!selected_map) {
+        return;
+    }
+
+    // used for map lookup
+    uint64_t cookie = bpf_get_socket_cookie(skb);
+    if (!cookie) return;
+
+    uint16_t sport = 0;
+    uint16_t dport = 0;
+    uint8_t protocol = 0;  // TODO: Use are reserved value? Or int (-1) and cast to uint below?
+    struct in6_addr src_ip = {};
+    struct in6_addr dst_ip = {};
+    uint8_t tos = 0;       // Only used for IPv4
+    uint8_t priority = 0;  // Only used for IPv6
+    uint8_t flow_lbl = 0;  // Only used for IPv6
+    if (ipv4) {
+        const struct iphdr* const iph = is_eth ? (void*)(eth + 1) : data;
+        hdr_size = l2_header_size + sizeof(struct iphdr);
+        // Must have ipv4 header
+        if (data + hdr_size > data_end) return;
+
+        // IP version must be 4
+        if (iph->version != 4) return;
+
+        // We cannot handle IP options, just standard 20 byte == 5 dword minimal IPv4 header
+        if (iph->ihl != 5) return;
+
+        // V4 mapped address in in6_addr sets 10/11 position to 0xff.
+        src_ip.s6_addr32[2] = htonl(0x0000ffff);
+        dst_ip.s6_addr32[2] = htonl(0x0000ffff);
+
+        // Copy IPv4 address into in6_addr for easy comparison below.
+        src_ip.s6_addr32[3] = iph->saddr;
+        dst_ip.s6_addr32[3] = iph->daddr;
+        protocol = iph->protocol;
+        tos = iph->tos;
+    } else {
+        struct ipv6hdr* ip6h = is_eth ? (void*)(eth + 1) : data;
+        hdr_size = l2_header_size + sizeof(struct ipv6hdr);
+        // Must have ipv6 header
+        if (data + hdr_size > data_end) return;
+
+        if (ip6h->version != 6) return;
+
+        src_ip = ip6h->saddr;
+        dst_ip = ip6h->daddr;
+        protocol = ip6h->nexthdr;
+        priority = ip6h->priority;
+        flow_lbl = ip6h->flow_lbl[0];
+    }
+
+    switch (protocol) {
+        case IPPROTO_UDP:
+        case IPPROTO_UDPLITE: {
+            struct udphdr* udp;
+            udp = data + hdr_size;
+            if ((void*)(udp + 1) > data_end) return;
+            sport = udp->source;
+            dport = udp->dest;
+        } break;
+        case IPPROTO_TCP: {
+            struct tcphdr* tcp;
+            tcp = data + hdr_size;
+            if ((void*)(tcp + 1) > data_end) return;
+            sport = tcp->source;
+            dport = tcp->dest;
+        } break;
+        default:
+            return;
+    }
+
+    RuleEntry* existing_rule;
+    if (ipv4) {
+        if (*selected_map == MAP_A) {
+            existing_rule = bpf_ipv4_socket_to_policies_map_A_lookup_elem(&cookie);
+        } else {
+            existing_rule = bpf_ipv4_socket_to_policies_map_B_lookup_elem(&cookie);
+        }
+    } else {
+        if (*selected_map == MAP_A) {
+            existing_rule = bpf_ipv6_socket_to_policies_map_A_lookup_elem(&cookie);
+        } else {
+            existing_rule = bpf_ipv6_socket_to_policies_map_B_lookup_elem(&cookie);
+        }
+    }
+
+    if (existing_rule && v6_equal(src_ip, existing_rule->src_ip) &&
+            v6_equal(dst_ip, existing_rule->dst_ip) && skb->ifindex == existing_rule->ifindex &&
+        ntohs(sport) == htons(existing_rule->src_port) &&
+        ntohs(dport) == htons(existing_rule->dst_port) && protocol == existing_rule->proto) {
+        if (ipv4) {
+            uint8_t newTos = UPDATE_TOS(existing_rule->dscp_val, tos);
+            bpf_l3_csum_replace(skb, IP4_OFFSET(check, l2_header_size), htons(tos), htons(newTos),
+                                sizeof(uint16_t));
+            bpf_skb_store_bytes(skb, IP4_OFFSET(tos, l2_header_size), &newTos, sizeof(newTos), 0);
+        } else {
+            uint8_t new_priority = UPDATE_PRIORITY(existing_rule->dscp_val);
+            uint8_t new_flow_label = UPDATE_FLOW_LABEL(existing_rule->dscp_val, flow_lbl);
+            bpf_skb_store_bytes(skb, 0 + l2_header_size, &new_priority, sizeof(uint8_t), 0);
+            bpf_skb_store_bytes(skb, 1 + l2_header_size, &new_flow_label, sizeof(uint8_t), 0);
+        }
+        return;
+    }
+
+    // Linear scan ipv4_dscp_policies_map since no stored params match skb.
+    int best_score = -1;
+    uint32_t best_match = 0;
+
+    for (register uint64_t i = 0; i < MAX_POLICIES; i++) {
+        int score = 0;
+        uint8_t temp_mask = 0;
+        // Using a uint64 in for loop prevents infinite loop during BPF load,
+        // but the key is uint32, so convert back.
+        uint32_t key = i;
+
+        DscpPolicy* policy;
+        if (ipv4) {
+            policy = bpf_ipv4_dscp_policies_map_lookup_elem(&key);
+        } else {
+            policy = bpf_ipv6_dscp_policies_map_lookup_elem(&key);
+        }
+
+        // If the policy lookup failed, present_fields is 0, or iface index does not match
+        // index on skb buff, then we can continue to next policy.
+        if (!policy || policy->present_fields == 0 || policy->ifindex != skb->ifindex) continue;
+
+        if ((policy->present_fields & SRC_IP_MASK_FLAG) == SRC_IP_MASK_FLAG &&
+            v6_equal(src_ip, policy->src_ip)) {
+            score++;
+            temp_mask |= SRC_IP_MASK_FLAG;
+        }
+        if ((policy->present_fields & DST_IP_MASK_FLAG) == DST_IP_MASK_FLAG &&
+            v6_equal(dst_ip, policy->dst_ip)) {
+            score++;
+            temp_mask |= DST_IP_MASK_FLAG;
+        }
+        if ((policy->present_fields & SRC_PORT_MASK_FLAG) == SRC_PORT_MASK_FLAG &&
+            ntohs(sport) == htons(policy->src_port)) {
+            score++;
+            temp_mask |= SRC_PORT_MASK_FLAG;
+        }
+        if ((policy->present_fields & DST_PORT_MASK_FLAG) == DST_PORT_MASK_FLAG &&
+            ntohs(dport) >= htons(policy->dst_port_start) &&
+            ntohs(dport) <= htons(policy->dst_port_end)) {
+            score++;
+            temp_mask |= DST_PORT_MASK_FLAG;
+        }
+        if ((policy->present_fields & PROTO_MASK_FLAG) == PROTO_MASK_FLAG &&
+            protocol == policy->proto) {
+            score++;
+            temp_mask |= PROTO_MASK_FLAG;
+        }
+
+        if (score > best_score && temp_mask == policy->present_fields) {
+            best_match = i;
+            best_score = score;
+        }
+    }
+
+    uint8_t new_tos = 0;  // Can 0 be used as default forwarding value?
+    uint8_t new_dscp = 0;
+    uint8_t new_priority = 0;
+    uint8_t new_flow_lbl = 0;
+    if (best_score > 0) {
+        DscpPolicy* policy;
+        if (ipv4) {
+            policy = bpf_ipv4_dscp_policies_map_lookup_elem(&best_match);
+        } else {
+            policy = bpf_ipv6_dscp_policies_map_lookup_elem(&best_match);
+        }
+
+        if (policy) {
+            new_dscp = policy->dscp_val;
+            if (ipv4) {
+                new_tos = UPDATE_TOS(new_dscp, tos);
+            } else {
+                new_priority = UPDATE_PRIORITY(new_dscp);
+                new_flow_lbl = UPDATE_FLOW_LABEL(new_dscp, flow_lbl);
+            }
+        }
+    } else
+        return;
+
+    RuleEntry value = {
+        .src_ip = src_ip,
+        .dst_ip = dst_ip,
+        .ifindex = skb->ifindex,
+        .src_port = sport,
+        .dst_port = dport,
+        .proto = protocol,
+        .dscp_val = new_dscp,
+    };
+
+    // Update map with new policy.
+    if (ipv4) {
+        if (*selected_map == MAP_A) {
+            bpf_ipv4_socket_to_policies_map_A_update_elem(&cookie, &value, BPF_ANY);
+        } else {
+            bpf_ipv4_socket_to_policies_map_B_update_elem(&cookie, &value, BPF_ANY);
+        }
+    } else {
+        if (*selected_map == MAP_A) {
+            bpf_ipv6_socket_to_policies_map_A_update_elem(&cookie, &value, BPF_ANY);
+        } else {
+            bpf_ipv6_socket_to_policies_map_B_update_elem(&cookie, &value, BPF_ANY);
+        }
+    }
+
+    // Need to store bytes after updating map or program will not load.
+    if (ipv4 && new_tos != (tos & 252)) {
+        bpf_l3_csum_replace(skb, IP4_OFFSET(check, l2_header_size), htons(tos), htons(new_tos), 2);
+        bpf_skb_store_bytes(skb, IP4_OFFSET(tos, l2_header_size), &new_tos, sizeof(new_tos), 0);
+    } else if (!ipv4 && (new_priority != priority || new_flow_lbl != flow_lbl)) {
+        bpf_skb_store_bytes(skb, l2_header_size, &new_priority, sizeof(new_priority), 0);
+        bpf_skb_store_bytes(skb, l2_header_size + 1, &new_flow_lbl, sizeof(new_flow_lbl), 0);
+    }
+    return;
+}
+
+DEFINE_BPF_PROG_KVER("schedcls/set_dscp_ether", AID_ROOT, AID_SYSTEM,
+                     schedcls_set_dscp_ether, KVER(5, 15, 0))
+(struct __sk_buff* skb) {
+    if (skb->pkt_type != PACKET_HOST) return TC_ACT_PIPE;
+
+    if (skb->protocol == htons(ETH_P_IP)) {
+        match_policy(skb, true, true);
+    } else if (skb->protocol == htons(ETH_P_IPV6)) {
+        match_policy(skb, false, true);
+    }
+
+    // Always return TC_ACT_PIPE
+    return TC_ACT_PIPE;
+}
+
+DEFINE_BPF_PROG_KVER("schedcls/set_dscp_raw_ip", AID_ROOT, AID_SYSTEM,
+                     schedcls_set_dscp_raw_ip, KVER(5, 15, 0))
+(struct __sk_buff* skb) {
+    if (skb->protocol == htons(ETH_P_IP)) {
+        match_policy(skb, true, false);
+    } else if (skb->protocol == htons(ETH_P_IPV6)) {
+        match_policy(skb, false, false);
+    }
+
+    // Always return TC_ACT_PIPE
+    return TC_ACT_PIPE;
+}
+
+LICENSE("Apache 2.0");
+CRITICAL("Connectivity");
diff --git a/bpf_progs/dscp_policy.h b/bpf_progs/dscpPolicy.h
similarity index 87%
rename from bpf_progs/dscp_policy.h
rename to bpf_progs/dscpPolicy.h
index 1637f7a..455a121 100644
--- a/bpf_progs/dscp_policy.h
+++ b/bpf_progs/dscpPolicy.h
@@ -44,27 +44,27 @@
         (void*)BPF_FUNC_skb_ecn_set_ce;
 
 typedef struct {
-    struct in6_addr srcIp;
-    struct in6_addr dstIp;
+    struct in6_addr src_ip;
+    struct in6_addr dst_ip;
     uint32_t ifindex;
-    __be16 srcPort;
-    __be16 dstPortStart;
-    __be16 dstPortEnd;
+    __be16 src_port;
+    __be16 dst_port_start;
+    __be16 dst_port_end;
     uint8_t proto;
-    uint8_t dscpVal;
-    uint8_t presentFields;
+    uint8_t dscp_val;
+    uint8_t present_fields;
     uint8_t pad[3];
 } DscpPolicy;
 STRUCT_SIZE(DscpPolicy, 2 * 16 + 4 + 3 * 2 + 3 * 1 + 3);  // 48
 
 typedef struct {
-    struct in6_addr srcIp;
-    struct in6_addr dstIp;
+    struct in6_addr src_ip;
+    struct in6_addr dst_ip;
     __u32 ifindex;
-    __be16 srcPort;
-    __be16 dstPort;
+    __be16 src_port;
+    __be16 dst_port;
     __u8 proto;
-    __u8 dscpVal;
+    __u8 dscp_val;
     __u8 pad[2];
 } RuleEntry;
 STRUCT_SIZE(RuleEntry, 2 * 16 + 1 * 4 + 2 * 2 + 2 * 1 + 2);  // 44
\ No newline at end of file
diff --git a/bpf_progs/dscp_policy.c b/bpf_progs/dscp_policy.c
deleted file mode 100644
index 538a9e4..0000000
--- a/bpf_progs/dscp_policy.c
+++ /dev/null
@@ -1,329 +0,0 @@
-/*
- * 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.
- */
-
-#include <linux/types.h>
-#include <linux/bpf.h>
-#include <linux/if_packet.h>
-#include <linux/ip.h>
-#include <linux/ipv6.h>
-#include <linux/if_ether.h>
-#include <linux/pkt_cls.h>
-#include <linux/tcp.h>
-#include <stdint.h>
-#include <netinet/in.h>
-#include <netinet/udp.h>
-#include <string.h>
-
-// The resulting .o needs to load on the Android T beta 3 bpfloader
-#define BPFLOADER_MIN_VER BPFLOADER_T_BETA3_VERSION
-
-#include "bpf_helpers.h"
-#include "dscp_policy.h"
-
-DEFINE_BPF_MAP_GRW(switch_comp_map, ARRAY, int, uint64_t, 1, AID_SYSTEM)
-
-DEFINE_BPF_MAP_GRW(ipv4_socket_to_policies_map_A, HASH, uint64_t, RuleEntry, MAX_POLICIES,
-        AID_SYSTEM)
-DEFINE_BPF_MAP_GRW(ipv4_socket_to_policies_map_B, HASH, uint64_t, RuleEntry, MAX_POLICIES,
-        AID_SYSTEM)
-DEFINE_BPF_MAP_GRW(ipv6_socket_to_policies_map_A, HASH, uint64_t, RuleEntry, MAX_POLICIES,
-        AID_SYSTEM)
-DEFINE_BPF_MAP_GRW(ipv6_socket_to_policies_map_B, HASH, uint64_t, RuleEntry, MAX_POLICIES,
-        AID_SYSTEM)
-
-DEFINE_BPF_MAP_GRW(ipv4_dscp_policies_map, ARRAY, uint32_t, DscpPolicy, MAX_POLICIES,
-        AID_SYSTEM)
-DEFINE_BPF_MAP_GRW(ipv6_dscp_policies_map, ARRAY, uint32_t, DscpPolicy, MAX_POLICIES,
-        AID_SYSTEM)
-
-static inline __always_inline void match_policy(struct __sk_buff* skb, bool ipv4, bool is_eth) {
-    void* data = (void*)(long)skb->data;
-    const void* data_end = (void*)(long)skb->data_end;
-
-    const int l2_header_size = is_eth ? sizeof(struct ethhdr) : 0;
-    struct ethhdr* eth = is_eth ? data : NULL;
-
-    if (data + l2_header_size > data_end) return;
-
-    int zero = 0;
-    int hdr_size = 0;
-    uint64_t* selectedMap = bpf_switch_comp_map_lookup_elem(&zero);
-
-    // use this with HASH map so map lookup only happens once policies have been added?
-    if (!selectedMap) {
-        return;
-    }
-
-    // used for map lookup
-    uint64_t cookie = bpf_get_socket_cookie(skb);
-    if (!cookie)
-        return;
-
-    uint16_t sport = 0;
-    uint16_t dport = 0;
-    uint8_t protocol = 0; // TODO: Use are reserved value? Or int (-1) and cast to uint below?
-    struct in6_addr srcIp = {};
-    struct in6_addr dstIp = {};
-    uint8_t tos = 0; // Only used for IPv4
-    uint8_t priority = 0; // Only used for IPv6
-    uint8_t flow_lbl = 0; // Only used for IPv6
-    if (ipv4) {
-        const struct iphdr* const iph = is_eth ? (void*)(eth + 1) : data;
-        // Must have ipv4 header
-        if (data + l2_header_size + sizeof(*iph) > data_end) return;
-
-        // IP version must be 4
-        if (iph->version != 4) return;
-
-        // We cannot handle IP options, just standard 20 byte == 5 dword minimal IPv4 header
-        if (iph->ihl != 5) return;
-
-        // V4 mapped address in in6_addr sets 10/11 position to 0xff.
-        srcIp.s6_addr32[2] = htonl(0x0000ffff);
-        dstIp.s6_addr32[2] = htonl(0x0000ffff);
-
-        // Copy IPv4 address into in6_addr for easy comparison below.
-        srcIp.s6_addr32[3] = iph->saddr;
-        dstIp.s6_addr32[3] = iph->daddr;
-        protocol = iph->protocol;
-        tos = iph->tos;
-        hdr_size = sizeof(struct iphdr);
-    } else {
-        struct ipv6hdr* ip6h = is_eth ? (void*)(eth + 1) : data;
-        // Must have ipv6 header
-        if (data + l2_header_size + sizeof(*ip6h) > data_end) return;
-
-        if (ip6h->version != 6) return;
-
-        srcIp = ip6h->saddr;
-        dstIp = ip6h->daddr;
-        protocol = ip6h->nexthdr;
-        priority = ip6h->priority;
-        flow_lbl = ip6h->flow_lbl[0];
-        hdr_size = sizeof(struct ipv6hdr);
-    }
-
-    switch (protocol) {
-        case IPPROTO_UDP:
-        case IPPROTO_UDPLITE:
-        {
-            struct udphdr *udp;
-            udp = data + hdr_size;
-            if ((void*)(udp + 1) > data_end) return;
-            sport = udp->source;
-            dport = udp->dest;
-        }
-        break;
-        case IPPROTO_TCP:
-        {
-            struct tcphdr *tcp;
-            tcp = data + hdr_size;
-            if ((void*)(tcp + 1) > data_end) return;
-            sport = tcp->source;
-            dport = tcp->dest;
-        }
-        break;
-        default:
-            return;
-    }
-
-    RuleEntry* existingRule;
-    if (ipv4) {
-        if (*selectedMap == MAP_A) {
-            existingRule = bpf_ipv4_socket_to_policies_map_A_lookup_elem(&cookie);
-        } else {
-            existingRule = bpf_ipv4_socket_to_policies_map_B_lookup_elem(&cookie);
-        }
-    } else {
-        if (*selectedMap == MAP_A) {
-            existingRule = bpf_ipv6_socket_to_policies_map_A_lookup_elem(&cookie);
-        } else {
-            existingRule = bpf_ipv6_socket_to_policies_map_B_lookup_elem(&cookie);
-        }
-    }
-
-    if (existingRule && v6_equal(srcIp, existingRule->srcIp) &&
-                v6_equal(dstIp, existingRule->dstIp) &&
-                skb->ifindex == existingRule->ifindex &&
-                ntohs(sport) == htons(existingRule->srcPort) &&
-                ntohs(dport) == htons(existingRule->dstPort) &&
-                protocol == existingRule->proto) {
-        if (ipv4) {
-            int ecn = tos & 3;
-            uint8_t newDscpVal = (existingRule->dscpVal << 2) + ecn;
-            int oldDscpVal = tos >> 2;
-            bpf_l3_csum_replace(skb, 1, oldDscpVal, newDscpVal, sizeof(uint8_t));
-            bpf_skb_store_bytes(skb, 1, &newDscpVal, sizeof(uint8_t), 0);
-        } else {
-            uint8_t new_priority = (existingRule->dscpVal >> 2) + 0x60;
-            uint8_t new_flow_label = ((existingRule->dscpVal & 0xf) << 6) + (priority >> 6);
-            bpf_skb_store_bytes(skb, 0, &new_priority, sizeof(uint8_t), 0);
-            bpf_skb_store_bytes(skb, 1, &new_flow_label, sizeof(uint8_t), 0);
-        }
-        return;
-    }
-
-    // Linear scan ipv4_dscp_policies_map since no stored params match skb.
-    int bestScore = -1;
-    uint32_t bestMatch = 0;
-
-    for (register uint64_t i = 0; i < MAX_POLICIES; i++) {
-        int score = 0;
-        uint8_t tempMask = 0;
-        // Using a uint64 in for loop prevents infinite loop during BPF load,
-        // but the key is uint32, so convert back.
-        uint32_t key = i;
-
-        DscpPolicy* policy;
-        if (ipv4) {
-            policy = bpf_ipv4_dscp_policies_map_lookup_elem(&key);
-        } else {
-            policy = bpf_ipv6_dscp_policies_map_lookup_elem(&key);
-        }
-
-        // If the policy lookup failed, presentFields is 0, or iface index does not match
-        // index on skb buff, then we can continue to next policy.
-        if (!policy || policy->presentFields == 0 || policy->ifindex != skb->ifindex)
-            continue;
-
-        if ((policy->presentFields & SRC_IP_MASK_FLAG) == SRC_IP_MASK_FLAG &&
-                v6_equal(srcIp, policy->srcIp)) {
-            score++;
-            tempMask |= SRC_IP_MASK_FLAG;
-        }
-        if ((policy->presentFields & DST_IP_MASK_FLAG) == DST_IP_MASK_FLAG &&
-                v6_equal(dstIp, policy->dstIp)) {
-            score++;
-            tempMask |= DST_IP_MASK_FLAG;
-        }
-        if ((policy->presentFields & SRC_PORT_MASK_FLAG) == SRC_PORT_MASK_FLAG &&
-                ntohs(sport) == htons(policy->srcPort)) {
-            score++;
-            tempMask |= SRC_PORT_MASK_FLAG;
-        }
-        if ((policy->presentFields & DST_PORT_MASK_FLAG) == DST_PORT_MASK_FLAG &&
-                ntohs(dport) >= htons(policy->dstPortStart) &&
-                ntohs(dport) <= htons(policy->dstPortEnd)) {
-            score++;
-            tempMask |= DST_PORT_MASK_FLAG;
-        }
-        if ((policy->presentFields & PROTO_MASK_FLAG) == PROTO_MASK_FLAG &&
-                protocol == policy->proto) {
-            score++;
-            tempMask |= PROTO_MASK_FLAG;
-        }
-
-        if (score > bestScore && tempMask == policy->presentFields) {
-            bestMatch = i;
-            bestScore = score;
-        }
-    }
-
-    uint8_t new_tos= 0; // Can 0 be used as default forwarding value?
-    uint8_t new_priority = 0;
-    uint8_t new_flow_lbl = 0;
-    if (bestScore > 0) {
-        DscpPolicy* policy;
-        if (ipv4) {
-            policy = bpf_ipv4_dscp_policies_map_lookup_elem(&bestMatch);
-        } else {
-            policy = bpf_ipv6_dscp_policies_map_lookup_elem(&bestMatch);
-        }
-
-        if (policy) {
-            // TODO: if DSCP value is already set ignore?
-            if (ipv4) {
-                int ecn = tos & 3;
-                new_tos = (policy->dscpVal << 2) + ecn;
-            } else {
-                new_priority = (policy->dscpVal >> 2) + 0x60;
-                new_flow_lbl = ((policy->dscpVal & 0xf) << 6) + (flow_lbl >> 6);
-
-                // Set IPv6 curDscp value to stored value and recalulate priority
-                // and flow label during next use.
-                new_tos = policy->dscpVal;
-            }
-        }
-    } else return;
-
-    RuleEntry value = {
-        .srcIp = srcIp,
-        .dstIp = dstIp,
-        .ifindex = skb->ifindex,
-        .srcPort = sport,
-        .dstPort = dport,
-        .proto = protocol,
-        .dscpVal = new_tos,
-    };
-
-    //Update map with new policy.
-    if (ipv4) {
-        if (*selectedMap == MAP_A) {
-            bpf_ipv4_socket_to_policies_map_A_update_elem(&cookie, &value, BPF_ANY);
-        } else {
-            bpf_ipv4_socket_to_policies_map_B_update_elem(&cookie, &value, BPF_ANY);
-        }
-    } else {
-        if (*selectedMap == MAP_A) {
-            bpf_ipv6_socket_to_policies_map_A_update_elem(&cookie, &value, BPF_ANY);
-        } else {
-            bpf_ipv6_socket_to_policies_map_B_update_elem(&cookie, &value, BPF_ANY);
-        }
-    }
-
-    // Need to store bytes after updating map or program will not load.
-    if (ipv4 && new_tos != (tos & 252)) {
-        int oldDscpVal = tos >> 2;
-        bpf_l3_csum_replace(skb, 1, oldDscpVal, new_tos, sizeof(uint8_t));
-        bpf_skb_store_bytes(skb, 1, &new_tos, sizeof(uint8_t), 0);
-    } else if (!ipv4 && (new_priority != priority || new_flow_lbl != flow_lbl)) {
-        bpf_skb_store_bytes(skb, 0, &new_priority, sizeof(uint8_t), 0);
-        bpf_skb_store_bytes(skb, 1, &new_flow_lbl, sizeof(uint8_t), 0);
-    }
-    return;
-}
-
-DEFINE_BPF_PROG_KVER("schedcls/set_dscp_ether", AID_ROOT, AID_SYSTEM,
-                     schedcls_set_dscp_ether, KVER(5, 4, 0))
-(struct __sk_buff* skb) {
-
-    if (skb->pkt_type != PACKET_HOST) return TC_ACT_PIPE;
-
-    if (skb->protocol == htons(ETH_P_IP)) {
-        match_policy(skb, true, true);
-    } else if (skb->protocol == htons(ETH_P_IPV6)) {
-        match_policy(skb, false, true);
-    }
-
-    // Always return TC_ACT_PIPE
-    return TC_ACT_PIPE;
-}
-
-DEFINE_BPF_PROG_KVER("schedcls/set_dscp_raw_ip", AID_ROOT, AID_SYSTEM,
-                     schedcls_set_dscp_raw_ip, KVER(5, 4, 0))
-(struct __sk_buff* skb) {
-    if (skb->protocol == htons(ETH_P_IP)) {
-        match_policy(skb, true, false);
-    } else if (skb->protocol == htons(ETH_P_IPV6)) {
-        match_policy(skb, false, false);
-    }
-
-    // Always return TC_ACT_PIPE
-    return TC_ACT_PIPE;
-}
-
-LICENSE("Apache 2.0");
-CRITICAL("Connectivity");
diff --git a/bpf_progs/offload.c b/bpf_progs/offload.c
index 2ec0792..4eb1e8d 100644
--- a/bpf_progs/offload.c
+++ b/bpf_progs/offload.c
@@ -24,8 +24,27 @@
 #define __kernel_udphdr udphdr
 #include <linux/udp.h>
 
+#ifdef BTF
+// BTF is incompatible with bpfloaders < v0.10, hence for S (v0.2) we must
+// ship a different file than for later versions, but we need bpfloader v0.25+
+// for obj@ver.o support
+#define BPFLOADER_MIN_VER BPFLOADER_OBJ_AT_VER_VERSION
+#else /* BTF */
 // The resulting .o needs to load on the Android S bpfloader
 #define BPFLOADER_MIN_VER BPFLOADER_S_VERSION
+#define BPFLOADER_MAX_VER BPFLOADER_OBJ_AT_VER_VERSION
+#endif /* BTF */
+
+// Warning: values other than AID_ROOT don't work for map uid on BpfLoader < v0.21
+#define TETHERING_UID AID_ROOT
+
+#ifdef INPROCESS
+#define DEFAULT_BPF_MAP_SELINUX_CONTEXT "fs_bpf_net_shared"
+#define DEFAULT_BPF_PROG_SELINUX_CONTEXT "fs_bpf_net_shared"
+#define TETHERING_GID AID_SYSTEM
+#else
+#define TETHERING_GID AID_NETWORK_STACK
+#endif
 
 #include "bpf_helpers.h"
 #include "bpf_net_helpers.h"
@@ -73,7 +92,7 @@
 // ----- Tethering Error Counters -----
 
 DEFINE_BPF_MAP_GRW(tether_error_map, ARRAY, uint32_t, uint32_t, BPF_TETHER_ERR__MAX,
-                   AID_NETWORK_STACK)
+                   TETHERING_GID)
 
 #define COUNT_AND_RETURN(counter, ret) do {                     \
     uint32_t code = BPF_TETHER_ERR_ ## counter;                 \
@@ -91,22 +110,22 @@
 // ----- Tethering Data Stats and Limits -----
 
 // Tethering stats, indexed by upstream interface.
-DEFINE_BPF_MAP_GRW(tether_stats_map, HASH, TetherStatsKey, TetherStatsValue, 16, AID_NETWORK_STACK)
+DEFINE_BPF_MAP_GRW(tether_stats_map, HASH, TetherStatsKey, TetherStatsValue, 16, TETHERING_GID)
 
 // Tethering data limit, indexed by upstream interface.
 // (tethering allowed when stats[iif].rxBytes + stats[iif].txBytes < limit[iif])
-DEFINE_BPF_MAP_GRW(tether_limit_map, HASH, TetherLimitKey, TetherLimitValue, 16, AID_NETWORK_STACK)
+DEFINE_BPF_MAP_GRW(tether_limit_map, HASH, TetherLimitKey, TetherLimitValue, 16, TETHERING_GID)
 
 // ----- IPv6 Support -----
 
 DEFINE_BPF_MAP_GRW(tether_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 64,
-                   AID_NETWORK_STACK)
+                   TETHERING_GID)
 
 DEFINE_BPF_MAP_GRW(tether_downstream64_map, HASH, TetherDownstream64Key, TetherDownstream64Value,
-                   1024, AID_NETWORK_STACK)
+                   1024, TETHERING_GID)
 
 DEFINE_BPF_MAP_GRW(tether_upstream6_map, HASH, TetherUpstream6Key, Tether6Value, 64,
-                   AID_NETWORK_STACK)
+                   TETHERING_GID)
 
 static inline __always_inline int do_forward6(struct __sk_buff* skb, const bool is_ethernet,
         const bool downstream) {
@@ -280,13 +299,13 @@
     return bpf_redirect(v->oif, 0 /* this is effectively BPF_F_EGRESS */);
 }
 
-DEFINE_BPF_PROG("schedcls/tether_downstream6_ether", AID_ROOT, AID_NETWORK_STACK,
+DEFINE_BPF_PROG("schedcls/tether_downstream6_ether", TETHERING_UID, TETHERING_GID,
                 sched_cls_tether_downstream6_ether)
 (struct __sk_buff* skb) {
     return do_forward6(skb, /* is_ethernet */ true, /* downstream */ true);
 }
 
-DEFINE_BPF_PROG("schedcls/tether_upstream6_ether", AID_ROOT, AID_NETWORK_STACK,
+DEFINE_BPF_PROG("schedcls/tether_upstream6_ether", TETHERING_UID, TETHERING_GID,
                 sched_cls_tether_upstream6_ether)
 (struct __sk_buff* skb) {
     return do_forward6(skb, /* is_ethernet */ true, /* downstream */ false);
@@ -305,13 +324,13 @@
 // and thus a 5.4 kernel always supports this.
 //
 // Hence, these mandatory (must load successfully) implementations for 5.4+ kernels:
-DEFINE_BPF_PROG_KVER("schedcls/tether_downstream6_rawip$5_4", AID_ROOT, AID_NETWORK_STACK,
+DEFINE_BPF_PROG_KVER("schedcls/tether_downstream6_rawip$5_4", TETHERING_UID, TETHERING_GID,
                      sched_cls_tether_downstream6_rawip_5_4, KVER(5, 4, 0))
 (struct __sk_buff* skb) {
     return do_forward6(skb, /* is_ethernet */ false, /* downstream */ true);
 }
 
-DEFINE_BPF_PROG_KVER("schedcls/tether_upstream6_rawip$5_4", AID_ROOT, AID_NETWORK_STACK,
+DEFINE_BPF_PROG_KVER("schedcls/tether_upstream6_rawip$5_4", TETHERING_UID, TETHERING_GID,
                      sched_cls_tether_upstream6_rawip_5_4, KVER(5, 4, 0))
 (struct __sk_buff* skb) {
     return do_forward6(skb, /* is_ethernet */ false, /* downstream */ false);
@@ -319,7 +338,7 @@
 
 // and these identical optional (may fail to load) implementations for [4.14..5.4) patched kernels:
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream6_rawip$4_14",
-                                    AID_ROOT, AID_NETWORK_STACK,
+                                    TETHERING_UID, TETHERING_GID,
                                     sched_cls_tether_downstream6_rawip_4_14,
                                     KVER(4, 14, 0), KVER(5, 4, 0))
 (struct __sk_buff* skb) {
@@ -327,7 +346,7 @@
 }
 
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream6_rawip$4_14",
-                                    AID_ROOT, AID_NETWORK_STACK,
+                                    TETHERING_UID, TETHERING_GID,
                                     sched_cls_tether_upstream6_rawip_4_14,
                                     KVER(4, 14, 0), KVER(5, 4, 0))
 (struct __sk_buff* skb) {
@@ -337,13 +356,13 @@
 // and define no-op stubs for [4.9,4.14) and unpatched [4.14,5.4) kernels.
 // (if the above real 4.14+ program loaded successfully, then bpfloader will have already pinned
 // it at the same location this one would be pinned at and will thus skip loading this stub)
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream6_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream6_rawip$stub", TETHERING_UID, TETHERING_GID,
                            sched_cls_tether_downstream6_rawip_stub, KVER_NONE, KVER(5, 4, 0))
 (struct __sk_buff* skb) {
     return TC_ACT_PIPE;
 }
 
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream6_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream6_rawip$stub", TETHERING_UID, TETHERING_GID,
                            sched_cls_tether_upstream6_rawip_stub, KVER_NONE, KVER(5, 4, 0))
 (struct __sk_buff* skb) {
     return TC_ACT_PIPE;
@@ -351,9 +370,9 @@
 
 // ----- IPv4 Support -----
 
-DEFINE_BPF_MAP_GRW(tether_downstream4_map, HASH, Tether4Key, Tether4Value, 1024, AID_NETWORK_STACK)
+DEFINE_BPF_MAP_GRW(tether_downstream4_map, HASH, Tether4Key, Tether4Value, 1024, TETHERING_GID)
 
-DEFINE_BPF_MAP_GRW(tether_upstream4_map, HASH, Tether4Key, Tether4Value, 1024, AID_NETWORK_STACK)
+DEFINE_BPF_MAP_GRW(tether_upstream4_map, HASH, Tether4Key, Tether4Value, 1024, TETHERING_GID)
 
 static inline __always_inline int do_forward4_bottom(struct __sk_buff* skb,
         const int l2_header_size, void* data, const void* data_end,
@@ -645,25 +664,25 @@
 
 // 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,
+DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_rawip$5_8", TETHERING_UID, TETHERING_GID,
                      sched_cls_tether_downstream4_rawip_5_8, KVER(5, 8, 0))
 (struct __sk_buff* skb) {
     return do_forward4(skb, /* is_ethernet */ false, /* downstream */ true, /* updatetime */ true);
 }
 
-DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_rawip$5_8", AID_ROOT, AID_NETWORK_STACK,
+DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_rawip$5_8", TETHERING_UID, TETHERING_GID,
                      sched_cls_tether_upstream4_rawip_5_8, KVER(5, 8, 0))
 (struct __sk_buff* skb) {
     return do_forward4(skb, /* is_ethernet */ false, /* downstream */ false, /* updatetime */ true);
 }
 
-DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_ether$5_8", AID_ROOT, AID_NETWORK_STACK,
+DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_ether$5_8", TETHERING_UID, TETHERING_GID,
                      sched_cls_tether_downstream4_ether_5_8, KVER(5, 8, 0))
 (struct __sk_buff* skb) {
     return do_forward4(skb, /* is_ethernet */ true, /* downstream */ true, /* updatetime */ true);
 }
 
-DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_ether$5_8", AID_ROOT, AID_NETWORK_STACK,
+DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_ether$5_8", TETHERING_UID, TETHERING_GID,
                      sched_cls_tether_upstream4_ether_5_8, KVER(5, 8, 0))
 (struct __sk_buff* skb) {
     return do_forward4(skb, /* is_ethernet */ true, /* downstream */ false, /* updatetime */ true);
@@ -673,7 +692,7 @@
 // (optional, because we need to be able to fallback for 4.14/4.19/5.4 pre-S kernels)
 
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$opt",
-                                    AID_ROOT, AID_NETWORK_STACK,
+                                    TETHERING_UID, TETHERING_GID,
                                     sched_cls_tether_downstream4_rawip_opt,
                                     KVER(4, 14, 0), KVER(5, 8, 0))
 (struct __sk_buff* skb) {
@@ -681,7 +700,7 @@
 }
 
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$opt",
-                                    AID_ROOT, AID_NETWORK_STACK,
+                                    TETHERING_UID, TETHERING_GID,
                                     sched_cls_tether_upstream4_rawip_opt,
                                     KVER(4, 14, 0), KVER(5, 8, 0))
 (struct __sk_buff* skb) {
@@ -689,7 +708,7 @@
 }
 
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$opt",
-                                    AID_ROOT, AID_NETWORK_STACK,
+                                    TETHERING_UID, TETHERING_GID,
                                     sched_cls_tether_downstream4_ether_opt,
                                     KVER(4, 14, 0), KVER(5, 8, 0))
 (struct __sk_buff* skb) {
@@ -697,7 +716,7 @@
 }
 
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$opt",
-                                    AID_ROOT, AID_NETWORK_STACK,
+                                    TETHERING_UID, TETHERING_GID,
                                     sched_cls_tether_upstream4_ether_opt,
                                     KVER(4, 14, 0), KVER(5, 8, 0))
 (struct __sk_buff* skb) {
@@ -718,13 +737,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,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$5_4", TETHERING_UID, TETHERING_GID,
                            sched_cls_tether_downstream4_rawip_5_4, KVER(5, 4, 0), KVER(5, 8, 0))
 (struct __sk_buff* skb) {
     return do_forward4(skb, /* is_ethernet */ false, /* downstream */ true, /* updatetime */ false);
 }
 
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$5_4", AID_ROOT, AID_NETWORK_STACK,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$5_4", TETHERING_UID, TETHERING_GID,
                            sched_cls_tether_upstream4_rawip_5_4, KVER(5, 4, 0), KVER(5, 8, 0))
 (struct __sk_buff* skb) {
     return do_forward4(skb, /* is_ethernet */ false, /* downstream */ false, /* updatetime */ false);
@@ -734,7 +753,7 @@
 // [Note: fallback for 4.14/4.19 (P/Q) kernels is below in stub section]
 
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$4_14",
-                                    AID_ROOT, AID_NETWORK_STACK,
+                                    TETHERING_UID, TETHERING_GID,
                                     sched_cls_tether_downstream4_rawip_4_14,
                                     KVER(4, 14, 0), KVER(5, 4, 0))
 (struct __sk_buff* skb) {
@@ -742,7 +761,7 @@
 }
 
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$4_14",
-                                    AID_ROOT, AID_NETWORK_STACK,
+                                    TETHERING_UID, TETHERING_GID,
                                     sched_cls_tether_upstream4_rawip_4_14,
                                     KVER(4, 14, 0), KVER(5, 4, 0))
 (struct __sk_buff* skb) {
@@ -751,13 +770,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,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$4_14", TETHERING_UID, TETHERING_GID,
                            sched_cls_tether_downstream4_ether_4_14, KVER(4, 14, 0), KVER(5, 8, 0))
 (struct __sk_buff* skb) {
     return do_forward4(skb, /* is_ethernet */ true, /* downstream */ true, /* updatetime */ false);
 }
 
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$4_14", AID_ROOT, AID_NETWORK_STACK,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$4_14", TETHERING_UID, TETHERING_GID,
                            sched_cls_tether_upstream4_ether_4_14, KVER(4, 14, 0), KVER(5, 8, 0))
 (struct __sk_buff* skb) {
     return do_forward4(skb, /* is_ethernet */ true, /* downstream */ false, /* updatetime */ false);
@@ -767,13 +786,13 @@
 
 // RAWIP: 4.9-P/Q, 4.14-P/Q & 4.19-Q kernels -- without bpf_skb_change_head() for tc programs
 
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$stub", TETHERING_UID, TETHERING_GID,
                            sched_cls_tether_downstream4_rawip_stub, KVER_NONE, KVER(5, 4, 0))
 (struct __sk_buff* skb) {
     return TC_ACT_PIPE;
 }
 
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$stub", TETHERING_UID, TETHERING_GID,
                            sched_cls_tether_upstream4_rawip_stub, KVER_NONE, KVER(5, 4, 0))
 (struct __sk_buff* skb) {
     return TC_ACT_PIPE;
@@ -781,13 +800,13 @@
 
 // ETHER: 4.9-P/Q kernel
 
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$stub", AID_ROOT, AID_NETWORK_STACK,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$stub", TETHERING_UID, TETHERING_GID,
                            sched_cls_tether_downstream4_ether_stub, KVER_NONE, KVER(4, 14, 0))
 (struct __sk_buff* skb) {
     return TC_ACT_PIPE;
 }
 
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$stub", AID_ROOT, AID_NETWORK_STACK,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$stub", TETHERING_UID, TETHERING_GID,
                            sched_cls_tether_upstream4_ether_stub, KVER_NONE, KVER(4, 14, 0))
 (struct __sk_buff* skb) {
     return TC_ACT_PIPE;
@@ -795,7 +814,7 @@
 
 // ----- XDP Support -----
 
-DEFINE_BPF_MAP_GRW(tether_dev_map, DEVMAP_HASH, uint32_t, uint32_t, 64, AID_NETWORK_STACK)
+DEFINE_BPF_MAP_GRW(tether_dev_map, DEVMAP_HASH, uint32_t, uint32_t, 64, TETHERING_GID)
 
 static inline __always_inline int do_xdp_forward6(struct xdp_md *ctx, const bool is_ethernet,
         const bool downstream) {
@@ -840,7 +859,7 @@
 }
 
 #define DEFINE_XDP_PROG(str, func) \
-    DEFINE_BPF_PROG_KVER(str, AID_ROOT, AID_NETWORK_STACK, func, KVER(5, 9, 0))(struct xdp_md *ctx)
+    DEFINE_BPF_PROG_KVER(str, TETHERING_UID, TETHERING_GID, func, KVER(5, 9, 0))(struct xdp_md *ctx)
 
 DEFINE_XDP_PROG("xdp/tether_downstream_ether",
                  xdp_tether_downstream_ether) {
diff --git a/bpf_progs/offload@btf.c b/bpf_progs/offload@btf.c
new file mode 120000
index 0000000..4092e0d
--- /dev/null
+++ b/bpf_progs/offload@btf.c
@@ -0,0 +1 @@
+offload.c
\ No newline at end of file
diff --git a/bpf_progs/offload@inprocess.c b/bpf_progs/offload@inprocess.c
new file mode 120000
index 0000000..4092e0d
--- /dev/null
+++ b/bpf_progs/offload@inprocess.c
@@ -0,0 +1 @@
+offload.c
\ No newline at end of file
diff --git a/bpf_progs/test.c b/bpf_progs/test.c
index f2fcc8c..d42205f 100644
--- a/bpf_progs/test.c
+++ b/bpf_progs/test.c
@@ -18,8 +18,27 @@
 #include <linux/in.h>
 #include <linux/ip.h>
 
+#ifdef BTF
+// BTF is incompatible with bpfloaders < v0.10, hence for S (v0.2) we must
+// ship a different file than for later versions, but we need bpfloader v0.25+
+// for obj@ver.o support
+#define BPFLOADER_MIN_VER BPFLOADER_OBJ_AT_VER_VERSION
+#else /* BTF */
 // The resulting .o needs to load on the Android S bpfloader
 #define BPFLOADER_MIN_VER BPFLOADER_S_VERSION
+#define BPFLOADER_MAX_VER BPFLOADER_OBJ_AT_VER_VERSION
+#endif /* BTF */
+
+// Warning: values other than AID_ROOT don't work for map uid on BpfLoader < v0.21
+#define TETHERING_UID AID_ROOT
+
+#ifdef INPROCESS
+#define DEFAULT_BPF_MAP_SELINUX_CONTEXT "fs_bpf_net_shared"
+#define DEFAULT_BPF_PROG_SELINUX_CONTEXT "fs_bpf_net_shared"
+#define TETHERING_GID AID_SYSTEM
+#else
+#define TETHERING_GID AID_NETWORK_STACK
+#endif
 
 #include "bpf_helpers.h"
 #include "bpf_net_helpers.h"
@@ -27,12 +46,11 @@
 
 // Used only by TetheringPrivilegedTests, not by production code.
 DEFINE_BPF_MAP_GRW(tether_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 16,
-                   AID_NETWORK_STACK)
+                   TETHERING_GID)
 // Used only by BpfBitmapTest, not by production code.
-DEFINE_BPF_MAP_GRW(bitmap, ARRAY, int, uint64_t, 2,
-                   AID_NETWORK_STACK)
+DEFINE_BPF_MAP_GRW(bitmap, ARRAY, int, uint64_t, 2, TETHERING_GID)
 
-DEFINE_BPF_PROG_KVER("xdp/drop_ipv4_udp_ether", AID_ROOT, AID_NETWORK_STACK,
+DEFINE_BPF_PROG_KVER("xdp/drop_ipv4_udp_ether", TETHERING_UID, TETHERING_GID,
                       xdp_test, KVER(5, 9, 0))
 (struct xdp_md *ctx) {
     void *data = (void *)(long)ctx->data;
diff --git a/bpf_progs/test@btf.c b/bpf_progs/test@btf.c
new file mode 120000
index 0000000..aeebb26
--- /dev/null
+++ b/bpf_progs/test@btf.c
@@ -0,0 +1 @@
+test.c
\ No newline at end of file
diff --git a/bpf_progs/test@inprocess.c b/bpf_progs/test@inprocess.c
new file mode 120000
index 0000000..aeebb26
--- /dev/null
+++ b/bpf_progs/test@inprocess.c
@@ -0,0 +1 @@
+test.c
\ No newline at end of file
diff --git a/common/Android.bp b/common/Android.bp
new file mode 100644
index 0000000..729ef32
--- /dev/null
+++ b/common/Android.bp
@@ -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.
+//
+
+package {
+    // See: http://go/android-license-faq
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "connectivity-net-module-utils-bpf",
+    srcs: [
+        "src/com/android/net/module/util/bpf/*.java",
+    ],
+    sdk_version: "module_current",
+    min_sdk_version: "29",
+    visibility: [
+        // Do not add any lib. This library is only shared inside connectivity module
+        // and its tests.
+        "//packages/modules/Connectivity:__subpackages__",
+    ],
+    libs: [
+        "androidx.annotation_annotation",
+        "framework-connectivity.stubs.module_lib",
+    ],
+    static_libs: [
+        "net-utils-device-common-struct",
+    ],
+    apex_available: [
+        "com.android.tethering",
+    ],
+    lint: { strict_updatability_linting: true },
+}
diff --git a/common/src/com/android/net/module/util/bpf/ClatEgress4Key.java b/common/src/com/android/net/module/util/bpf/ClatEgress4Key.java
new file mode 100644
index 0000000..12200ec
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/ClatEgress4Key.java
@@ -0,0 +1,37 @@
+/*
+ * 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.bpf;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.Inet4Address;
+
+/** Key type for clat egress IPv4 maps. */
+public class ClatEgress4Key extends Struct {
+    @Field(order = 0, type = Type.U32)
+    public final long iif; // The input interface index
+
+    @Field(order = 1, type = Type.Ipv4Address)
+    public final Inet4Address local4; // The source IPv4 address
+
+    public ClatEgress4Key(final long iif, final Inet4Address local4) {
+        this.iif = iif;
+        this.local4 = local4;
+    }
+}
diff --git a/common/src/com/android/net/module/util/bpf/ClatEgress4Value.java b/common/src/com/android/net/module/util/bpf/ClatEgress4Value.java
new file mode 100644
index 0000000..c10cb4d
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/ClatEgress4Value.java
@@ -0,0 +1,46 @@
+/*
+ * 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.bpf;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.Inet6Address;
+
+/** Value type for clat egress IPv4 maps. */
+public class ClatEgress4Value extends Struct {
+    @Field(order = 0, type = Type.U32)
+    public final long oif; // The output interface to redirect to
+
+    @Field(order = 1, type = Type.Ipv6Address)
+    public final Inet6Address local6; // The full 128-bits of the source IPv6 address
+
+    @Field(order = 2, type = Type.Ipv6Address)
+    public final Inet6Address pfx96; // The destination /96 nat64 prefix, bottom 32 bits must be 0
+
+    @Field(order = 3, type = Type.U8, padding = 3)
+    public final short oifIsEthernet; // Whether the output interface requires ethernet header
+
+    public ClatEgress4Value(final long oif, final Inet6Address local6, final Inet6Address pfx96,
+            final short oifIsEthernet) {
+        this.oif = oif;
+        this.local6 = local6;
+        this.pfx96 = pfx96;
+        this.oifIsEthernet = oifIsEthernet;
+    }
+}
diff --git a/common/src/com/android/net/module/util/bpf/ClatIngress6Key.java b/common/src/com/android/net/module/util/bpf/ClatIngress6Key.java
new file mode 100644
index 0000000..1e2f4e0
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/ClatIngress6Key.java
@@ -0,0 +1,41 @@
+/*
+ * 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.bpf;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.Inet6Address;
+
+/** Key type for clat ingress IPv6 maps. */
+public class ClatIngress6Key extends Struct {
+    @Field(order = 0, type = Type.U32)
+    public final long iif; // The input interface index
+
+    @Field(order = 1, type = Type.Ipv6Address)
+    public final Inet6Address pfx96; // The source /96 nat64 prefix, bottom 32 bits must be 0
+
+    @Field(order = 2, type = Type.Ipv6Address)
+    public final Inet6Address local6; // The full 128-bits of the destination IPv6 address
+
+    public ClatIngress6Key(final long iif, final Inet6Address pfx96, final Inet6Address local6) {
+        this.iif = iif;
+        this.pfx96 = pfx96;
+        this.local6 = local6;
+    }
+}
diff --git a/common/src/com/android/net/module/util/bpf/ClatIngress6Value.java b/common/src/com/android/net/module/util/bpf/ClatIngress6Value.java
new file mode 100644
index 0000000..bfec44f
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/ClatIngress6Value.java
@@ -0,0 +1,37 @@
+/*
+ * 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.bpf;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.Inet4Address;
+
+/** Value type for clat ingress IPv6 maps. */
+public class ClatIngress6Value extends Struct {
+    @Field(order = 0, type = Type.U32)
+    public final long oif; // The output interface to redirect to (0 means don't redirect)
+
+    @Field(order = 1, type = Type.Ipv4Address)
+    public final Inet4Address local4; // The destination IPv4 address
+
+    public ClatIngress6Value(final long oif, final Inet4Address local4) {
+        this.oif = oif;
+        this.local4 = local4;
+    }
+}
diff --git a/common/src/com/android/net/module/util/bpf/Tether4Key.java b/common/src/com/android/net/module/util/bpf/Tether4Key.java
new file mode 100644
index 0000000..638576f
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/Tether4Key.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.bpf;
+
+import android.net.MacAddress;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.Inet4Address;
+import java.net.UnknownHostException;
+import java.util.Objects;
+
+/** Key type for downstream & upstream IPv4 forwarding maps. */
+public class Tether4Key extends Struct {
+    @Field(order = 0, type = Type.U32)
+    public final long iif;
+
+    @Field(order = 1, type = Type.EUI48)
+    public final MacAddress dstMac;
+
+    @Field(order = 2, type = Type.U8, padding = 1)
+    public final short l4proto;
+
+    @Field(order = 3, type = Type.ByteArray, arraysize = 4)
+    public final byte[] src4;
+
+    @Field(order = 4, type = Type.ByteArray, arraysize = 4)
+    public final byte[] dst4;
+
+    @Field(order = 5, type = Type.UBE16)
+    public final int srcPort;
+
+    @Field(order = 6, type = Type.UBE16)
+    public final int dstPort;
+
+    public Tether4Key(final long iif, @NonNull final MacAddress dstMac, final short l4proto,
+            final byte[] src4, final byte[] dst4, final int srcPort,
+            final int dstPort) {
+        Objects.requireNonNull(dstMac);
+
+        this.iif = iif;
+        this.dstMac = dstMac;
+        this.l4proto = l4proto;
+        this.src4 = src4;
+        this.dst4 = dst4;
+        this.srcPort = srcPort;
+        this.dstPort = dstPort;
+    }
+
+    @Override
+    public String toString() {
+        try {
+            return String.format(
+                    "iif: %d, dstMac: %s, l4proto: %d, src4: %s, dst4: %s, "
+                            + "srcPort: %d, dstPort: %d",
+                    iif, dstMac, l4proto,
+                    Inet4Address.getByAddress(src4), Inet4Address.getByAddress(dst4),
+                    Short.toUnsignedInt((short) srcPort), Short.toUnsignedInt((short) dstPort));
+        } catch (UnknownHostException | IllegalArgumentException e) {
+            return String.format("Invalid IP address", e);
+        }
+    }
+}
diff --git a/common/src/com/android/net/module/util/bpf/Tether4Value.java b/common/src/com/android/net/module/util/bpf/Tether4Value.java
new file mode 100644
index 0000000..de98766
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/Tether4Value.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.bpf;
+
+import android.net.MacAddress;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Objects;
+
+/** Value type for downstream & upstream IPv4 forwarding maps. */
+public class Tether4Value extends Struct {
+    @Field(order = 0, type = Type.U32)
+    public final long oif;
+
+    // The ethhdr struct which is defined in uapi/linux/if_ether.h
+    @Field(order = 1, type = Type.EUI48)
+    public final MacAddress ethDstMac;
+    @Field(order = 2, type = Type.EUI48)
+    public final MacAddress ethSrcMac;
+    @Field(order = 3, type = Type.UBE16)
+    public final int ethProto;  // Packet type ID field.
+
+    @Field(order = 4, type = Type.U16)
+    public final int pmtu;
+
+    @Field(order = 5, type = Type.ByteArray, arraysize = 16)
+    public final byte[] src46;
+
+    @Field(order = 6, type = Type.ByteArray, arraysize = 16)
+    public final byte[] dst46;
+
+    @Field(order = 7, type = Type.UBE16)
+    public final int srcPort;
+
+    @Field(order = 8, type = Type.UBE16)
+    public final int dstPort;
+
+    // TODO: consider using U64.
+    @Field(order = 9, type = Type.U63)
+    public final long lastUsed;
+
+    public Tether4Value(final long oif, @NonNull final MacAddress ethDstMac,
+            @NonNull final MacAddress ethSrcMac, final int ethProto, final int pmtu,
+            final byte[] src46, final byte[] dst46, final int srcPort,
+            final int dstPort, final long lastUsed) {
+        Objects.requireNonNull(ethDstMac);
+        Objects.requireNonNull(ethSrcMac);
+
+        this.oif = oif;
+        this.ethDstMac = ethDstMac;
+        this.ethSrcMac = ethSrcMac;
+        this.ethProto = ethProto;
+        this.pmtu = pmtu;
+        this.src46 = src46;
+        this.dst46 = dst46;
+        this.srcPort = srcPort;
+        this.dstPort = dstPort;
+        this.lastUsed = lastUsed;
+    }
+
+    @Override
+    public String toString() {
+        try {
+            return String.format(
+                    "oif: %d, ethDstMac: %s, ethSrcMac: %s, ethProto: %d, pmtu: %d, "
+                            + "src46: %s, dst46: %s, srcPort: %d, dstPort: %d, "
+                            + "lastUsed: %d",
+                    oif, ethDstMac, ethSrcMac, ethProto, pmtu,
+                    InetAddress.getByAddress(src46), InetAddress.getByAddress(dst46),
+                    Short.toUnsignedInt((short) srcPort), Short.toUnsignedInt((short) dstPort),
+                    lastUsed);
+        } catch (UnknownHostException | IllegalArgumentException e) {
+            return String.format("Invalid IP address", e);
+        }
+    }
+}
diff --git a/common/src/com/android/net/module/util/bpf/TetherStatsKey.java b/common/src/com/android/net/module/util/bpf/TetherStatsKey.java
new file mode 100644
index 0000000..c6d595b
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/TetherStatsKey.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.bpf;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+/** The key of BpfMap which is used for tethering stats. */
+public class TetherStatsKey extends Struct {
+    @Field(order = 0, type = Type.U32)
+    public final long ifindex;  // upstream interface index
+
+    public TetherStatsKey(final long ifindex) {
+        this.ifindex = ifindex;
+    }
+
+    // TODO: remove equals, hashCode and toString once aosp/1536721 is merged.
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+
+        if (!(obj instanceof TetherStatsKey)) return false;
+
+        final TetherStatsKey that = (TetherStatsKey) obj;
+
+        return ifindex == that.ifindex;
+    }
+
+    @Override
+    public int hashCode() {
+        return Long.hashCode(ifindex);
+    }
+
+    @Override
+    public String toString() {
+        return String.format("ifindex: %d", ifindex);
+    }
+}
diff --git a/common/src/com/android/net/module/util/bpf/TetherStatsValue.java b/common/src/com/android/net/module/util/bpf/TetherStatsValue.java
new file mode 100644
index 0000000..028d217
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/TetherStatsValue.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.bpf;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+/** The key of BpfMap which is used for tethering stats. */
+public class TetherStatsValue extends Struct {
+    // Use the signed long variable to store the uint64 stats from stats BPF map.
+    // U63 is enough for each data element even at 5Gbps for ~468 years.
+    // 2^63 / (5 * 1000 * 1000 * 1000) * 8 / 86400 / 365 = 468.
+    @Field(order = 0, type = Type.U63)
+    public final long rxPackets;
+    @Field(order = 1, type = Type.U63)
+    public final long rxBytes;
+    @Field(order = 2, type = Type.U63)
+    public final long rxErrors;
+    @Field(order = 3, type = Type.U63)
+    public final long txPackets;
+    @Field(order = 4, type = Type.U63)
+    public final long txBytes;
+    @Field(order = 5, type = Type.U63)
+    public final long txErrors;
+
+    public TetherStatsValue(final long rxPackets, final long rxBytes, final long rxErrors,
+            final long txPackets, final long txBytes, final long txErrors) {
+        this.rxPackets = rxPackets;
+        this.rxBytes = rxBytes;
+        this.rxErrors = rxErrors;
+        this.txPackets = txPackets;
+        this.txBytes = txBytes;
+        this.txErrors = txErrors;
+    }
+
+    // TODO: remove equals, hashCode and toString once aosp/1536721 is merged.
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+
+        if (!(obj instanceof TetherStatsValue)) return false;
+
+        final TetherStatsValue that = (TetherStatsValue) obj;
+
+        return rxPackets == that.rxPackets
+                && rxBytes == that.rxBytes
+                && rxErrors == that.rxErrors
+                && txPackets == that.txPackets
+                && txBytes == that.txBytes
+                && txErrors == that.txErrors;
+    }
+
+    @Override
+    public int hashCode() {
+        return Long.hashCode(rxPackets) ^ Long.hashCode(rxBytes) ^ Long.hashCode(rxErrors)
+                ^ Long.hashCode(txPackets) ^ Long.hashCode(txBytes) ^ Long.hashCode(txErrors);
+    }
+
+    @Override
+    public String toString() {
+        return String.format("rxPackets: %s, rxBytes: %s, rxErrors: %s, txPackets: %s, "
+                + "txBytes: %s, txErrors: %s", rxPackets, rxBytes, rxErrors, txPackets,
+                txBytes, txErrors);
+    }
+}
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index 1e508a0..8c32ded 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -103,7 +103,7 @@
     // Do not add static_libs to this library: put them in framework-connectivity instead.
     // The jarjar rules are only so that references to jarjared utils in
     // framework-connectivity-pre-jarjar match at runtime.
-    jarjar_rules: ":connectivity-jarjar-rules",
+    jarjar_rules: ":framework-connectivity-jarjar-rules",
     permitted_packages: [
         "android.app.usage",
         "android.net",
diff --git a/framework-t/Sources.bp b/framework-t/Sources.bp
index b30ee80..391a562 100644
--- a/framework-t/Sources.bp
+++ b/framework-t/Sources.bp
@@ -14,125 +14,13 @@
 // limitations under the License.
 //
 
-// NetworkStats related libraries.
-
-filegroup {
-    name: "framework-connectivity-netstats-internal-sources",
-    srcs: [
-        "src/android/app/usage/*.java",
-        "src/android/net/DataUsageRequest.*",
-        "src/android/net/INetworkStatsService.aidl",
-        "src/android/net/INetworkStatsSession.aidl",
-        "src/android/net/NetworkIdentity.java",
-        "src/android/net/NetworkIdentitySet.java",
-        "src/android/net/NetworkStateSnapshot.*",
-        "src/android/net/NetworkStats.*",
-        "src/android/net/NetworkStatsAccess.*",
-        "src/android/net/NetworkStatsCollection.*",
-        "src/android/net/NetworkStatsHistory.*",
-        "src/android/net/NetworkTemplate.*",
-        "src/android/net/TrafficStats.java",
-        "src/android/net/UnderlyingNetworkInfo.*",
-        "src/android/net/netstats/**/*.*",
-    ],
-    path: "src",
-    visibility: [
-        "//visibility:private",
-    ],
-}
-
-filegroup {
-    name: "framework-connectivity-netstats-sources",
-    srcs: [
-        ":framework-connectivity-netstats-internal-sources",
-    ],
-    visibility: [
-        "//visibility:private",
-    ],
-}
-
-// Nsd related libraries.
-
-filegroup {
-    name: "framework-connectivity-nsd-internal-sources",
-    srcs: [
-        "src/android/net/nsd/*.aidl",
-        "src/android/net/nsd/*.java",
-    ],
-    path: "src",
-    visibility: [
-        "//visibility:private",
-    ],
-}
-
-filegroup {
-    name: "framework-connectivity-nsd-sources",
-    srcs: [
-        ":framework-connectivity-nsd-internal-sources",
-    ],
-    visibility: [
-        "//visibility:private",
-    ],
-}
-
-// IpSec related libraries.
-
-filegroup {
-    name: "framework-connectivity-ipsec-sources",
-    srcs: [
-        "src/android/net/IIpSecService.aidl",
-        "src/android/net/IpSec*.*",
-    ],
-    path: "src",
-    visibility: [
-        "//visibility:private",
-    ],
-}
-
-// Ethernet related libraries.
-
-filegroup {
-    name: "framework-connectivity-ethernet-sources",
-    srcs: [
-        "src/android/net/EthernetManager.java",
-        "src/android/net/EthernetNetworkManagementException.java",
-        "src/android/net/EthernetNetworkManagementException.aidl",
-        "src/android/net/EthernetNetworkSpecifier.java",
-        "src/android/net/EthernetNetworkUpdateRequest.java",
-        "src/android/net/EthernetNetworkUpdateRequest.aidl",
-        "src/android/net/IEthernetManager.aidl",
-        "src/android/net/IEthernetServiceListener.aidl",
-        "src/android/net/INetworkInterfaceOutcomeReceiver.aidl",
-        "src/android/net/ITetheredInterfaceCallback.aidl",
-    ],
-    path: "src",
-    visibility: [
-        "//visibility:private",
-    ],
-}
-
-// Connectivity-T common libraries.
-
-filegroup {
-    name: "framework-connectivity-tiramisu-internal-sources",
-    srcs: [
-        "src/android/net/ConnectivityFrameworkInitializerTiramisu.java",
-    ],
-    path: "src",
-    visibility: [
-        "//visibility:private",
-    ],
-}
-
 filegroup {
     name: "framework-connectivity-tiramisu-updatable-sources",
     srcs: [
-        ":framework-connectivity-ethernet-sources",
-        ":framework-connectivity-ipsec-sources",
-        ":framework-connectivity-netstats-sources",
-        ":framework-connectivity-nsd-sources",
-        ":framework-connectivity-tiramisu-internal-sources",
+        "src/**/*.java",
+        "src/**/*.aidl",
     ],
+    path: "src",
     visibility: [
         "//frameworks/base",
         "//packages/modules/Connectivity:__subpackages__",
diff --git a/framework-t/src/android/app/usage/NetworkStats.java b/framework-t/src/android/app/usage/NetworkStats.java
index 74fe4bd..26841de 100644
--- a/framework-t/src/android/app/usage/NetworkStats.java
+++ b/framework-t/src/android/app/usage/NetworkStats.java
@@ -1,17 +1,17 @@
 /**
  * Copyright (C) 2015 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
+ * 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
+ *      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.
+ * 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.app.usage;
@@ -36,11 +36,11 @@
 import java.util.ArrayList;
 
 /**
- * Class providing enumeration over buckets of network usage statistics. {@link NetworkStats} objects
- * are returned as results to various queries in {@link NetworkStatsManager}.
+ * Class providing enumeration over buckets of network usage statistics. {@link NetworkStats}
+ * objects are returned as results to various queries in {@link NetworkStatsManager}.
  */
 public final class NetworkStats implements AutoCloseable {
-    private final static String TAG = "NetworkStats";
+    private static final String TAG = "NetworkStats";
 
     private final CloseGuard mCloseGuard = CloseGuard.get();
 
@@ -616,7 +616,7 @@
     /**
      * Steps to next uid in enumeration and collects history for that.
      */
-    private void stepHistory(){
+    private void stepHistory() {
         if (hasNextUid()) {
             stepUid();
             mHistory = null;
@@ -692,8 +692,8 @@
                 bucketOut.mMetered = Bucket.METERED_ALL;
                 bucketOut.mRoaming = Bucket.ROAMING_ALL;
                 bucketOut.mBeginTimeStamp = mRecycledHistoryEntry.bucketStart;
-                bucketOut.mEndTimeStamp = mRecycledHistoryEntry.bucketStart +
-                        mRecycledHistoryEntry.bucketDuration;
+                bucketOut.mEndTimeStamp = mRecycledHistoryEntry.bucketStart
+                        + mRecycledHistoryEntry.bucketDuration;
                 bucketOut.mRxBytes = mRecycledHistoryEntry.rxBytes;
                 bucketOut.mRxPackets = mRecycledHistoryEntry.rxPackets;
                 bucketOut.mTxBytes = mRecycledHistoryEntry.txBytes;
diff --git a/framework-t/src/android/app/usage/NetworkStatsManager.java b/framework-t/src/android/app/usage/NetworkStatsManager.java
index f41475b..d139544 100644
--- a/framework-t/src/android/app/usage/NetworkStatsManager.java
+++ b/framework-t/src/android/app/usage/NetworkStatsManager.java
@@ -19,6 +19,9 @@
 import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.NetworkStats.METERED_YES;
+import static android.net.NetworkTemplate.MATCH_MOBILE;
+import static android.net.NetworkTemplate.MATCH_WIFI;
 
 import android.Manifest;
 import android.annotation.CallbackExecutor;
@@ -55,6 +58,7 @@
 
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.Executor;
 
 /**
@@ -1020,14 +1024,17 @@
         switch (networkType) {
             case ConnectivityManager.TYPE_MOBILE:
                 template = subscriberId == null
-                        ? NetworkTemplate.buildTemplateMobileWildcard()
-                        : NetworkTemplate.buildTemplateMobileAll(subscriberId);
+                        ? new NetworkTemplate.Builder(MATCH_MOBILE)
+                                .setMeteredness(METERED_YES).build()
+                        : new NetworkTemplate.Builder(MATCH_MOBILE)
+                                .setMeteredness(METERED_YES)
+                                .setSubscriberIds(Set.of(subscriberId)).build();
                 break;
             case ConnectivityManager.TYPE_WIFI:
                 template = TextUtils.isEmpty(subscriberId)
-                        ? NetworkTemplate.buildTemplateWifiWildcard()
-                        : NetworkTemplate.buildTemplateWifi(NetworkTemplate.WIFI_NETWORKID_ALL,
-                                subscriberId);
+                        ? new NetworkTemplate.Builder(MATCH_WIFI).build()
+                        : new  NetworkTemplate.Builder(MATCH_WIFI)
+                                .setSubscriberIds(Set.of(subscriberId)).build();
                 break;
             default:
                 throw new IllegalArgumentException("Cannot create template for network type "
diff --git a/framework-t/src/android/net/NetworkStats.java b/framework-t/src/android/net/NetworkStats.java
index 0bb98f8..a655a9b 100644
--- a/framework-t/src/android/net/NetworkStats.java
+++ b/framework-t/src/android/net/NetworkStats.java
@@ -1041,7 +1041,7 @@
      */
     public long getTotalPackets() {
         long total = 0;
-        for (int i = size-1; i >= 0; i--) {
+        for (int i = size - 1; i >= 0; i--) {
             total += rxPackets[i] + txPackets[i];
         }
         return total;
diff --git a/framework-t/src/android/net/netstats/provider/NetworkStatsProvider.java b/framework-t/src/android/net/netstats/provider/NetworkStatsProvider.java
index d37a53d..66d99a1 100644
--- a/framework-t/src/android/net/netstats/provider/NetworkStatsProvider.java
+++ b/framework-t/src/android/net/netstats/provider/NetworkStatsProvider.java
@@ -118,7 +118,7 @@
      *
      * @param token the token under which these stats were gathered. Providers can call this method
      *              with the current token as often as they want, until the token changes.
-     *              {@see NetworkStatsProvider#onRequestStatsUpdate()}
+     *              See {@link NetworkStatsProvider#onRequestStatsUpdate(int)}
      * @param ifaceStats the {@link NetworkStats} per interface to be reported.
      *                   The provider should not include any traffic that is already counted by
      *                   kernel interface counters.
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index fad63e5..3fcc11b 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -126,7 +126,7 @@
  * http://www.iana.org/form/ports-service. Existing services can be found at
  * http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xml
  *
- * {@see NsdServiceInfo}
+ * @see NsdServiceInfo
  */
 @SystemService(Context.NSD_SERVICE)
 public final class NsdManager {
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
index 200c808..6438a60 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -34,7 +34,7 @@
 
 /**
  * A class representing service information for network service discovery
- * {@see NsdManager}
+ * @see NsdManager
  */
 public final class NsdServiceInfo implements Parcelable {
 
diff --git a/framework/Android.bp b/framework/Android.bp
index 24d8cca..fcce7a5 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -92,6 +92,7 @@
         "framework-connectivity-javastream-protos",
     ],
     libs: [
+        "androidx.annotation_annotation",
         "app-compat-annotations",
         "framework-connectivity-t.stubs.module_lib",
         "unsupportedappusage",
@@ -111,6 +112,7 @@
         // because the tethering stubs depend on the connectivity stubs (e.g.,
         // TetheringRequest depends on LinkAddress).
         "framework-tethering.stubs.module_lib",
+        "framework-wifi.stubs.module_lib",
     ],
     visibility: ["//packages/modules/Connectivity:__subpackages__"]
 }
@@ -119,7 +121,7 @@
     name: "framework-connectivity",
     defaults: ["framework-connectivity-defaults"],
     installable: true,
-    jarjar_rules: ":connectivity-jarjar-rules",
+    jarjar_rules: ":framework-connectivity-jarjar-rules",
     permitted_packages: ["android.net"],
     impl_library_visibility: [
         "//packages/modules/Connectivity/Tethering/apex",
@@ -210,3 +212,33 @@
         "com.android.tethering",
     ],
 }
+
+java_genrule {
+    name: "framework-connectivity-jarjar-rules",
+    tool_files: [
+        ":connectivity-hiddenapi-files",
+        ":framework-connectivity-pre-jarjar{.jar}",
+        ":framework-connectivity-t-pre-jarjar{.jar}",
+        ":framework-connectivity.stubs.module_lib{.jar}",
+        ":framework-connectivity-t.stubs.module_lib{.jar}",
+        "jarjar-excludes.txt",
+    ],
+    tools: [
+        "jarjar-rules-generator",
+    ],
+    out: ["framework_connectivity_jarjar_rules.txt"],
+    cmd: "$(location jarjar-rules-generator) " +
+        "--jars $(location :framework-connectivity-pre-jarjar{.jar}) " +
+        "$(location :framework-connectivity-t-pre-jarjar{.jar}) " +
+        "--prefix android.net.connectivity " +
+        "--apistubs $(location :framework-connectivity.stubs.module_lib{.jar}) " +
+        "$(location :framework-connectivity-t.stubs.module_lib{.jar}) " +
+        "--unsupportedapi $(locations :connectivity-hiddenapi-files) " +
+        "--excludes $(location jarjar-excludes.txt) " +
+        "--output $(out)",
+    visibility: [
+        "//packages/modules/Connectivity/framework:__subpackages__",
+        "//packages/modules/Connectivity/framework-t:__subpackages__",
+        "//packages/modules/Connectivity/service",
+    ],
+}
diff --git a/framework/jni/android_net_NetworkUtils.cpp b/framework/jni/android_net_NetworkUtils.cpp
index 7478b3e..857ece5 100644
--- a/framework/jni/android_net_NetworkUtils.cpp
+++ b/framework/jni/android_net_NetworkUtils.cpp
@@ -232,7 +232,8 @@
         return NULL;
     }
 
-    jclass class_TcpRepairWindow = env->FindClass("android/net/TcpRepairWindow");
+    jclass class_TcpRepairWindow = env->FindClass(
+        "android/net/connectivity/android/net/TcpRepairWindow");
     jmethodID ctor = env->GetMethodID(class_TcpRepairWindow, "<init>", "(IIIIII)V");
 
     return env->NewObject(class_TcpRepairWindow, ctor, trw.snd_wl1, trw.snd_wnd, trw.max_window,
@@ -253,7 +254,7 @@
     { "bindSocketToNetworkHandle", "(Ljava/io/FileDescriptor;J)I", (void*) android_net_utils_bindSocketToNetworkHandle },
     { "attachDropAllBPFFilter", "(Ljava/io/FileDescriptor;)V", (void*) android_net_utils_attachDropAllBPFFilter },
     { "detachBPFFilter", "(Ljava/io/FileDescriptor;)V", (void*) android_net_utils_detachBPFFilter },
-    { "getTcpRepairWindow", "(Ljava/io/FileDescriptor;)Landroid/net/TcpRepairWindow;", (void*) android_net_utils_getTcpRepairWindow },
+    { "getTcpRepairWindow", "(Ljava/io/FileDescriptor;)Landroid/net/connectivity/android/net/TcpRepairWindow;", (void*) android_net_utils_getTcpRepairWindow },
     { "resNetworkSend", "(J[BII)Ljava/io/FileDescriptor;", (void*) android_net_utils_resNetworkSend },
     { "resNetworkQuery", "(JLjava/lang/String;III)Ljava/io/FileDescriptor;", (void*) android_net_utils_resNetworkQuery },
     { "resNetworkResult", "(Ljava/io/FileDescriptor;)Landroid/net/DnsResolver$DnsResponse;", (void*) android_net_utils_resNetworkResult },
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 02083ff..6ccd77e 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -556,7 +556,7 @@
      *
      * @deprecated Applications should instead use {@link NetworkCapabilities#hasTransport} or
      *         {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request an
-     *         appropriate network. {@see NetworkCapabilities} for supported transports.
+     *         appropriate network. See {@link NetworkCapabilities} for supported transports.
      */
     @Deprecated
     public static final int TYPE_MOBILE      = 0;
@@ -566,7 +566,7 @@
      *
      * @deprecated Applications should instead use {@link NetworkCapabilities#hasTransport} or
      *         {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request an
-     *         appropriate network. {@see NetworkCapabilities} for supported transports.
+     *         appropriate network. See {@link NetworkCapabilities} for supported transports.
      */
     @Deprecated
     public static final int TYPE_WIFI        = 1;
@@ -617,7 +617,7 @@
      *
      * @deprecated Applications should instead use {@link NetworkCapabilities#hasTransport} or
      *         {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request an
-     *         appropriate network. {@see NetworkCapabilities} for supported transports.
+     *         appropriate network. See {@link NetworkCapabilities} for supported transports.
      */
     @Deprecated
     public static final int TYPE_MOBILE_HIPRI = 5;
@@ -627,7 +627,7 @@
      *
      * @deprecated Applications should instead use {@link NetworkCapabilities#hasTransport} or
      *         {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request an
-     *         appropriate network. {@see NetworkCapabilities} for supported transports.
+     *         appropriate network. See {@link NetworkCapabilities} for supported transports.
      */
     @Deprecated
     public static final int TYPE_WIMAX       = 6;
@@ -637,7 +637,7 @@
      *
      * @deprecated Applications should instead use {@link NetworkCapabilities#hasTransport} or
      *         {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request an
-     *         appropriate network. {@see NetworkCapabilities} for supported transports.
+     *         appropriate network. See {@link NetworkCapabilities} for supported transports.
      */
     @Deprecated
     public static final int TYPE_BLUETOOTH   = 7;
@@ -654,7 +654,7 @@
      *
      * @deprecated Applications should instead use {@link NetworkCapabilities#hasTransport} or
      *         {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request an
-     *         appropriate network. {@see NetworkCapabilities} for supported transports.
+     *         appropriate network. See {@link NetworkCapabilities} for supported transports.
      */
     @Deprecated
     public static final int TYPE_ETHERNET    = 9;
@@ -1204,7 +1204,7 @@
 
     /**
      * Preference for {@link ProfileNetworkPreference#setPreference(int)}.
-     * {@see #setProfileNetworkPreferences(UserHandle, List, Executor, Runnable)}
+     * See {@link #setProfileNetworkPreferences(UserHandle, List, Executor, Runnable)}
      * Specify that the traffic for this user should by follow the default rules.
      * @hide
      */
@@ -1213,7 +1213,7 @@
 
     /**
      * Preference for {@link ProfileNetworkPreference#setPreference(int)}.
-     * {@see #setProfileNetworkPreferences(UserHandle, List, Executor, Runnable)}
+     * See {@link #setProfileNetworkPreferences(UserHandle, List, Executor, Runnable)}
      * Specify that the traffic for this user should by default go on a network with
      * {@link NetworkCapabilities#NET_CAPABILITY_ENTERPRISE}, and on the system default network
      * if no such network is available.
@@ -1224,7 +1224,7 @@
 
     /**
      * Preference for {@link ProfileNetworkPreference#setPreference(int)}.
-     * {@see #setProfileNetworkPreferences(UserHandle, List, Executor, Runnable)}
+     * See {@link #setProfileNetworkPreferences(UserHandle, List, Executor, Runnable)}
      * Specify that the traffic for this user should by default go on a network with
      * {@link NetworkCapabilities#NET_CAPABILITY_ENTERPRISE} and if no such network is available
      * should not go on the system default network
@@ -3383,8 +3383,8 @@
      * proxy is likely to break networking on multiple networks. This method is only meant
      * for device policy clients looking to do general internal filtering or similar use cases.
      *
-     * {@see #getGlobalProxy}
-     * {@see LinkProperties#getHttpProxy}
+     * @see #getGlobalProxy
+     * @see LinkProperties#getHttpProxy
      *
      * @param p A {@link ProxyInfo} object defining the new global HTTP proxy. Calling this
      *          method with a {@code null} value will clear the global HTTP proxy.
@@ -4277,7 +4277,7 @@
      * network, unless it becomes the best again at some later time. All callbacks are invoked
      * in order on the same thread, which by default is a thread created by the framework running
      * in the app.
-     * {@see #requestNetwork(NetworkRequest, NetworkCallback, Handler)} to change where the
+     * See {@link #requestNetwork(NetworkRequest, NetworkCallback, Handler)} to change where the
      * callbacks are invoked.
      *
      * <p>This{@link NetworkRequest} will live until released via
@@ -5922,7 +5922,7 @@
     }
 
     /**
-     * Get the specified firewall chain status.
+     * Get the specified firewall chain's status.
      *
      * @param chain target chain.
      * @return {@code true} if chain is enabled, {@code false} if chain is disabled.
diff --git a/framework/src/android/net/DnsResolver.java b/framework/src/android/net/DnsResolver.java
index 164160f..5e637f9 100644
--- a/framework/src/android/net/DnsResolver.java
+++ b/framework/src/android/net/DnsResolver.java
@@ -137,7 +137,7 @@
          * @param answer <T> answer to the query.
          * @param rcode The response code in the DNS response.
          *
-         * {@see android.net.DnsResolver#query query()}
+         * @see android.net.DnsResolver#query query()
          */
         void onAnswer(@NonNull T answer, int rcode);
         /**
diff --git a/framework/src/android/net/DnsResolverServiceManager.java b/framework/src/android/net/DnsResolverServiceManager.java
index 79009e8..e64d2ae 100644
--- a/framework/src/android/net/DnsResolverServiceManager.java
+++ b/framework/src/android/net/DnsResolverServiceManager.java
@@ -29,7 +29,7 @@
 
     private final IBinder mResolver;
 
-    DnsResolverServiceManager(IBinder resolver) {
+    public DnsResolverServiceManager(IBinder resolver) {
         mResolver = resolver;
     }
 
diff --git a/framework/src/android/net/ITestNetworkManager.aidl b/framework/src/android/net/ITestNetworkManager.aidl
index d18b931..9432acb 100644
--- a/framework/src/android/net/ITestNetworkManager.aidl
+++ b/framework/src/android/net/ITestNetworkManager.aidl
@@ -30,7 +30,8 @@
 interface ITestNetworkManager
 {
     TestNetworkInterface createInterface(boolean isTun, boolean hasCarrier, boolean bringUp,
-            in LinkAddress[] addrs, in @nullable String iface);
+            boolean disableIpv6ProvisioningDelay, in LinkAddress[] addrs,
+            in @nullable String iface);
 
     void setCarrierEnabled(in TestNetworkInterface iface, boolean enabled);
 
diff --git a/framework/src/android/net/LinkProperties.java b/framework/src/android/net/LinkProperties.java
index a8f707e..b7ee846 100644
--- a/framework/src/android/net/LinkProperties.java
+++ b/framework/src/android/net/LinkProperties.java
@@ -29,6 +29,8 @@
 import android.text.TextUtils;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.LinkPropertiesUtils;
 
 import java.net.Inet4Address;
@@ -42,7 +44,6 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.StringJoiner;
-import java.util.stream.Collectors;
 
 /**
  * Describes the properties of a network link.
@@ -759,9 +760,15 @@
      * @return An unmodifiable {@link List} of {@link RouteInfo} for this link.
      */
     public @NonNull List<RouteInfo> getRoutes() {
-        if (CompatChanges.isChangeEnabled(EXCLUDED_ROUTES)) {
+        // Before T, there's no throw routes because VpnService is not updatable, so no need to
+        // filter them out.
+        if (CompatChanges.isChangeEnabled(EXCLUDED_ROUTES) || !SdkLevel.isAtLeastT()) {
             return Collections.unmodifiableList(mRoutes);
         } else {
+            // Apps that added a throw route themselves (not obtaining LinkProperties from the
+            // system) will not see it in getRoutes on T+ if they do not have the compat change
+            // enabled (target SDK < T); but this is expected to be rare and typically only affect
+            // tests creating LinkProperties themselves (like CTS v12, which is only running on S).
             return Collections.unmodifiableList(getUnicastRoutes());
         }
     }
@@ -770,9 +777,7 @@
      * Returns all the {@link RouteInfo} of type {@link RouteInfo#RTN_UNICAST} set on this link.
      */
     private @NonNull List<RouteInfo> getUnicastRoutes() {
-        return mRoutes.stream()
-                .filter(route -> route.getType() == RouteInfo.RTN_UNICAST)
-                .collect(Collectors.toList());
+        return CollectionUtils.filter(mRoutes, route -> route.getType() == RouteInfo.RTN_UNICAST);
     }
 
     /**
diff --git a/framework/src/android/net/NattSocketKeepalive.java b/framework/src/android/net/NattSocketKeepalive.java
index a15d165..56cc923 100644
--- a/framework/src/android/net/NattSocketKeepalive.java
+++ b/framework/src/android/net/NattSocketKeepalive.java
@@ -33,7 +33,7 @@
     @NonNull private final InetAddress mDestination;
     private final int mResourceId;
 
-    NattSocketKeepalive(@NonNull IConnectivityManager service,
+    public NattSocketKeepalive(@NonNull IConnectivityManager service,
             @NonNull Network network,
             @NonNull ParcelFileDescriptor pfd,
             int resourceId,
@@ -48,7 +48,7 @@
     }
 
     @Override
-    void startImpl(int intervalSec) {
+    protected void startImpl(int intervalSec) {
         mExecutor.execute(() -> {
             try {
                 mService.startNattKeepaliveWithFd(mNetwork, mPfd, mResourceId,
@@ -62,7 +62,7 @@
     }
 
     @Override
-    void stopImpl() {
+    protected void stopImpl() {
         mExecutor.execute(() -> {
             try {
                 if (mSlot != null) {
diff --git a/framework/src/android/net/NetworkAgent.java b/framework/src/android/net/NetworkAgent.java
index 5659a35..1486619 100644
--- a/framework/src/android/net/NetworkAgent.java
+++ b/framework/src/android/net/NetworkAgent.java
@@ -84,7 +84,7 @@
  * the correct packets. Devices typically have a small number of slots
  * per radio technology, and the specific number of slots for each
  * technology is specified in configuration files.
- * {@see SocketKeepalive} for details.
+ * See {@link SocketKeepalive} for details.
  *
  * @hide
  */
diff --git a/framework/src/android/net/NetworkAgentConfig.java b/framework/src/android/net/NetworkAgentConfig.java
index 0d2b620..b6f3314 100644
--- a/framework/src/android/net/NetworkAgentConfig.java
+++ b/framework/src/android/net/NetworkAgentConfig.java
@@ -252,7 +252,7 @@
 
     /**
      * Whether network validation should be performed for this VPN network.
-     * {@see #isVpnValidationRequired}
+     * @see #isVpnValidationRequired
      * @hide
      */
     private boolean mVpnRequiresValidation = false;
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index 97b1f32..ea8a3df 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -192,7 +192,7 @@
     /**
      * Bitfield representing the network's enterprise capability identifier.  If any are specified
      * they will be satisfied by any Network that matches all of them.
-     * {@see addEnterpriseId} for details on how masks are added
+     * See {@link #addEnterpriseId(int)} for details on how masks are added
      */
     private int mEnterpriseId;
 
@@ -1460,7 +1460,7 @@
      * Sets the upstream bandwidth for this network in Kbps.  This always only refers to
      * the estimated first hop transport bandwidth.
      * <p>
-     * {@see Builder#setLinkUpstreamBandwidthKbps}
+     * @see Builder#setLinkUpstreamBandwidthKbps
      *
      * @param upKbps the estimated first hop upstream (device to network) bandwidth.
      * @hide
@@ -1484,7 +1484,7 @@
      * Sets the downstream bandwidth for this network in Kbps.  This always only refers to
      * the estimated first hop transport bandwidth.
      * <p>
-     * {@see Builder#setLinkUpstreamBandwidthKbps}
+     * @see Builder#setLinkUpstreamBandwidthKbps
      *
      * @param downKbps the estimated first hop downstream (network to device) bandwidth.
      * @hide
@@ -2534,7 +2534,7 @@
     /**
      * Set the uid and package name of the app causing this network to exist.
      *
-     * {@see #setRequestorUid} and {@link #setRequestorPackageName}
+     * See {@link #setRequestorUid} and {@link #setRequestorPackageName}
      *
      * @param uid UID of the app.
      * @param packageName package name of the app.
@@ -2719,7 +2719,7 @@
         /**
          * Removes the given transport type.
          *
-         * {@see #addTransportType}.
+         * @see #addTransportType
          *
          * @param transportType the transport type to be added or removed.
          * @return this builder
@@ -3043,7 +3043,7 @@
          * <p>
          * This list cannot be null, but it can be empty to mean that no UID without the
          * {@link android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS} permission
-         * gets to access this network.
+         * can access this network.
          *
          * @param uids the list of UIDs that can always access this network
          * @return this builder
diff --git a/framework/src/android/net/NetworkProvider.java b/framework/src/android/net/NetworkProvider.java
index 0665af5..3615075 100644
--- a/framework/src/android/net/NetworkProvider.java
+++ b/framework/src/android/net/NetworkProvider.java
@@ -192,21 +192,36 @@
     private class NetworkOfferCallbackProxy extends INetworkOfferCallback.Stub {
         @NonNull public final NetworkOfferCallback callback;
         @NonNull private final Executor mExecutor;
+        /**
+         * Boolean flag that prevents onNetworkNeeded / onNetworkUnneeded callbacks from being
+         * propagated after unregisterNetworkOffer has been called. Since unregisterNetworkOffer
+         * runs on the CS handler thread, it will not go into effect immediately.
+         */
+        private volatile boolean mIsStale;
 
         NetworkOfferCallbackProxy(@NonNull final NetworkOfferCallback callback,
                 @NonNull final Executor executor) {
             this.callback = callback;
             this.mExecutor = executor;
+            this.mIsStale = false;
         }
 
         @Override
         public void onNetworkNeeded(final @NonNull NetworkRequest request) {
-            mExecutor.execute(() -> callback.onNetworkNeeded(request));
+            mExecutor.execute(() -> {
+                if (!mIsStale) callback.onNetworkNeeded(request);
+            });
         }
 
         @Override
         public void onNetworkUnneeded(final @NonNull NetworkRequest request) {
-            mExecutor.execute(() -> callback.onNetworkUnneeded(request));
+            mExecutor.execute(() -> {
+                if (!mIsStale) callback.onNetworkUnneeded(request);
+            });
+        }
+
+        public void markStale() {
+            mIsStale = true;
         }
     }
 
@@ -326,7 +341,10 @@
     public void unregisterNetworkOffer(final @NonNull NetworkOfferCallback callback) {
         final NetworkOfferCallbackProxy proxy = findProxyForCallback(callback);
         if (null == proxy) return;
-        mProxies.remove(proxy);
+        synchronized (mProxies) {
+            proxy.markStale();
+            mProxies.remove(proxy);
+        }
         mContext.getSystemService(ConnectivityManager.class).unofferNetwork(proxy);
     }
 }
diff --git a/framework/src/android/net/ProfileNetworkPreference.java b/framework/src/android/net/ProfileNetworkPreference.java
index fdcab02..8b98721 100644
--- a/framework/src/android/net/ProfileNetworkPreference.java
+++ b/framework/src/android/net/ProfileNetworkPreference.java
@@ -79,7 +79,7 @@
      * if included is not empty, then only included UIDs are applied.
      * if excluded is not empty, then it is all uids in the user profile except these UIDs.
      * @return Array of uids included for the profile preference.
-     * {@see #getExcludedUids()}
+     * @see #getExcludedUids()
      */
     public @NonNull int[] getIncludedUids() {
         return mIncludedUids.clone();
@@ -93,7 +93,7 @@
      * <ul>If included is not empty, then only included UIDs are applied.</ul>
      * <ul>If excluded is not empty, then it is all uids in the user profile except these UIDs.</ul>
      * @return Array of uids not included for the profile preference.
-     * {@see #getIncludedUids()}
+     * @see #getIncludedUids()
      */
     public @NonNull int[] getExcludedUids() {
         return mExcludedUids.clone();
@@ -177,7 +177,7 @@
         /**
          * This is a array of uids for which profile perefence is set.
          * Empty would mean that this preference applies to all uids in the profile.
-         * {@see #setExcludedUids(int[])}
+         * @see #setExcludedUids(int[])
          * Included UIDs and Excluded UIDs can't both be non-empty.
          * if both are empty, it means this request applies to all uids in the user profile.
          * if included is not empty, then only included UIDs are applied.
@@ -195,7 +195,7 @@
 
         /**
          * This is a array of uids that are excluded for the profile perefence.
-         * {@see #setIncludedUids(int[])}
+         * @see #setIncludedUids(int[])
          * Included UIDs and Excluded UIDs can't both be non-empty.
          * if both are empty, it means this request applies to all uids in the user profile.
          * if included is not empty, then only included UIDs are applied.
diff --git a/framework/src/android/net/QosCallbackConnection.java b/framework/src/android/net/QosCallbackConnection.java
index de0fc24..cfceddd 100644
--- a/framework/src/android/net/QosCallbackConnection.java
+++ b/framework/src/android/net/QosCallbackConnection.java
@@ -35,7 +35,7 @@
  *
  * @hide
  */
-class QosCallbackConnection extends android.net.IQosCallback.Stub {
+public class QosCallbackConnection extends android.net.IQosCallback.Stub {
 
     @NonNull private final ConnectivityManager mConnectivityManager;
     @Nullable private volatile QosCallback mCallback;
@@ -56,7 +56,7 @@
      *                 {@link Executor} must run callback sequentially, otherwise the order of
      *                 callbacks cannot be guaranteed.
      */
-    QosCallbackConnection(@NonNull final ConnectivityManager connectivityManager,
+    public QosCallbackConnection(@NonNull final ConnectivityManager connectivityManager,
             @NonNull final QosCallback callback,
             @NonNull final Executor executor) {
         mConnectivityManager = Objects.requireNonNull(connectivityManager,
@@ -142,7 +142,7 @@
      * There are no synchronization guarantees on exactly when the callback will stop receiving
      * messages.
      */
-    void stopReceivingMessages() {
+    public void stopReceivingMessages() {
         mCallback = null;
     }
 }
diff --git a/framework/src/android/net/QosCallbackException.java b/framework/src/android/net/QosCallbackException.java
index b80cff4..7de3dd1 100644
--- a/framework/src/android/net/QosCallbackException.java
+++ b/framework/src/android/net/QosCallbackException.java
@@ -57,6 +57,9 @@
     private static final String TAG = "QosCallbackException";
 
     // Types of exceptions supported //
+    // The constants are used for the sendQosCallbackError system API, so they must not be changed
+    // as there may be callers relying on their historical values to call that API.
+    // TODO: mark the constants as @SystemApi, since they are necessary to call a system API.
     /** {@hide} */
     public static final int EX_TYPE_FILTER_NONE = 0;
 
@@ -67,13 +70,13 @@
     public static final int EX_TYPE_FILTER_SOCKET_NOT_BOUND = 2;
 
     /** {@hide} */
-    public static final int EX_TYPE_FILTER_SOCKET_NOT_CONNECTED = 3;
+    public static final int EX_TYPE_FILTER_NOT_SUPPORTED = 3;
 
     /** {@hide} */
-    public static final int EX_TYPE_FILTER_NOT_SUPPORTED = 4;
+    public static final int EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED = 4;
 
     /** {@hide} */
-    public static final int EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED = 5;
+    public static final int EX_TYPE_FILTER_SOCKET_NOT_CONNECTED = 5;
 
     /** {@hide} */
     public static final int EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED = 6;
@@ -85,7 +88,7 @@
      * {@hide}
      */
     @NonNull
-    static QosCallbackException createException(@ExceptionType final int type) {
+    public static QosCallbackException createException(@ExceptionType final int type) {
         switch (type) {
             case EX_TYPE_FILTER_NETWORK_RELEASED:
                 return new QosCallbackException(new NetworkReleasedException());
diff --git a/framework/src/android/net/QosFilter.java b/framework/src/android/net/QosFilter.java
index b432644..01dc4bb 100644
--- a/framework/src/android/net/QosFilter.java
+++ b/framework/src/android/net/QosFilter.java
@@ -33,13 +33,15 @@
 @SystemApi
 public abstract class QosFilter {
 
-    /**
-     * The constructor is kept hidden from outside this package to ensure that all derived types
-     * are known and properly handled when being passed to and from {@link NetworkAgent}.
-     *
-     * @hide
-     */
-    QosFilter() {
+    /** @hide */
+    protected QosFilter() {
+        // Ensure that all derived types are known, and known to be properly handled when being
+        // passed to and from NetworkAgent.
+        // For now the only known derived type is QosSocketFilter.
+        if (!(this instanceof QosSocketFilter)) {
+            throw new UnsupportedOperationException(
+                    "Unsupported QosFilter type: " + this.getClass().getName());
+        }
     }
 
     /**
diff --git a/framework/src/android/net/QosSocketInfo.java b/framework/src/android/net/QosSocketInfo.java
index 49ac22b..da9b356 100644
--- a/framework/src/android/net/QosSocketInfo.java
+++ b/framework/src/android/net/QosSocketInfo.java
@@ -73,9 +73,10 @@
      * The parcel file descriptor wrapped around the socket's file descriptor.
      *
      * @return the parcel file descriptor of the socket
+     * @hide
      */
     @NonNull
-    ParcelFileDescriptor getParcelFileDescriptor() {
+    public ParcelFileDescriptor getParcelFileDescriptor() {
         return mParcelFileDescriptor;
     }
 
diff --git a/framework/src/android/net/SocketKeepalive.java b/framework/src/android/net/SocketKeepalive.java
index f6cae72..57cf5e3 100644
--- a/framework/src/android/net/SocketKeepalive.java
+++ b/framework/src/android/net/SocketKeepalive.java
@@ -52,7 +52,8 @@
  * request. If it does, it MUST support at least 3 concurrent keepalive slots.
  */
 public abstract class SocketKeepalive implements AutoCloseable {
-    static final String TAG = "SocketKeepalive";
+    /** @hide */
+    protected static final String TAG = "SocketKeepalive";
 
     /**
      * Success. It indicates there is no error.
@@ -215,15 +216,22 @@
         }
     }
 
-    @NonNull final IConnectivityManager mService;
-    @NonNull final Network mNetwork;
-    @NonNull final ParcelFileDescriptor mPfd;
-    @NonNull final Executor mExecutor;
-    @NonNull final ISocketKeepaliveCallback mCallback;
+    /** @hide */
+    @NonNull protected final IConnectivityManager mService;
+    /** @hide */
+    @NonNull protected final Network mNetwork;
+    /** @hide */
+    @NonNull protected final ParcelFileDescriptor mPfd;
+    /** @hide */
+    @NonNull protected final Executor mExecutor;
+    /** @hide */
+    @NonNull protected final ISocketKeepaliveCallback mCallback;
     // TODO: remove slot since mCallback could be used to identify which keepalive to stop.
-    @Nullable Integer mSlot;
+    /** @hide */
+    @Nullable protected Integer mSlot;
 
-    SocketKeepalive(@NonNull IConnectivityManager service, @NonNull Network network,
+    /** @hide */
+    public SocketKeepalive(@NonNull IConnectivityManager service, @NonNull Network network,
             @NonNull ParcelFileDescriptor pfd,
             @NonNull Executor executor, @NonNull Callback callback) {
         mService = service;
@@ -303,7 +311,8 @@
         startImpl(intervalSec);
     }
 
-    abstract void startImpl(int intervalSec);
+    /** @hide */
+    protected abstract void startImpl(int intervalSec);
 
     /**
      * Requests that keepalive be stopped. The application must wait for {@link Callback#onStopped}
@@ -313,7 +322,8 @@
         stopImpl();
     }
 
-    abstract void stopImpl();
+    /** @hide */
+    protected abstract void stopImpl();
 
     /**
      * Deactivate this {@link SocketKeepalive} and free allocated resources. The instance won't be
diff --git a/framework/src/android/net/TcpSocketKeepalive.java b/framework/src/android/net/TcpSocketKeepalive.java
index d89814d..7131784 100644
--- a/framework/src/android/net/TcpSocketKeepalive.java
+++ b/framework/src/android/net/TcpSocketKeepalive.java
@@ -24,9 +24,9 @@
 import java.util.concurrent.Executor;
 
 /** @hide */
-final class TcpSocketKeepalive extends SocketKeepalive {
+public final class TcpSocketKeepalive extends SocketKeepalive {
 
-    TcpSocketKeepalive(@NonNull IConnectivityManager service,
+    public TcpSocketKeepalive(@NonNull IConnectivityManager service,
             @NonNull Network network,
             @NonNull ParcelFileDescriptor pfd,
             @NonNull Executor executor,
@@ -50,7 +50,7 @@
      *   acknowledgement.
      */
     @Override
-    void startImpl(int intervalSec) {
+    protected void startImpl(int intervalSec) {
         mExecutor.execute(() -> {
             try {
                 mService.startTcpKeepalive(mNetwork, mPfd, intervalSec, mCallback);
@@ -62,7 +62,7 @@
     }
 
     @Override
-    void stopImpl() {
+    protected void stopImpl() {
         mExecutor.execute(() -> {
             try {
                 if (mSlot != null) {
diff --git a/framework/src/android/net/TestNetworkManager.java b/framework/src/android/net/TestNetworkManager.java
index 9cae9e6..b64299f 100644
--- a/framework/src/android/net/TestNetworkManager.java
+++ b/framework/src/android/net/TestNetworkManager.java
@@ -59,6 +59,8 @@
     private static final boolean TUN = true;
     private static final boolean BRING_UP = true;
     private static final boolean CARRIER_UP = true;
+    // sets disableIpv6ProvisioningDelay to false.
+    private static final boolean USE_IPV6_PROV_DELAY = false;
     private static final LinkAddress[] NO_ADDRS = new LinkAddress[0];
 
     /** @hide */
@@ -167,8 +169,8 @@
     public TestNetworkInterface createTunInterface(@NonNull Collection<LinkAddress> linkAddrs) {
         try {
             final LinkAddress[] arr = new LinkAddress[linkAddrs.size()];
-            return mService.createInterface(TUN, CARRIER_UP, BRING_UP, linkAddrs.toArray(arr),
-                    null /* iface */);
+            return mService.createInterface(TUN, CARRIER_UP, BRING_UP, USE_IPV6_PROV_DELAY,
+                    linkAddrs.toArray(arr), null /* iface */);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -186,7 +188,27 @@
     @NonNull
     public TestNetworkInterface createTapInterface() {
         try {
-            return mService.createInterface(TAP, CARRIER_UP, BRING_UP, NO_ADDRS, null /* iface */);
+            return mService.createInterface(TAP, CARRIER_UP, BRING_UP, USE_IPV6_PROV_DELAY,
+                    NO_ADDRS, null /* iface */);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * 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();
         }
@@ -205,7 +227,8 @@
     @NonNull
     public TestNetworkInterface createTapInterface(boolean bringUp) {
         try {
-            return mService.createInterface(TAP, CARRIER_UP, bringUp, NO_ADDRS, null /* iface */);
+            return mService.createInterface(TAP, CARRIER_UP, bringUp, USE_IPV6_PROV_DELAY,
+                    NO_ADDRS, null /* iface */);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -227,7 +250,8 @@
     @NonNull
     public TestNetworkInterface createTapInterface(boolean bringUp, @NonNull String iface) {
         try {
-            return mService.createInterface(TAP, CARRIER_UP, bringUp, NO_ADDRS, iface);
+            return mService.createInterface(TAP, CARRIER_UP, bringUp, USE_IPV6_PROV_DELAY,
+                    NO_ADDRS, iface);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -246,7 +270,49 @@
     @NonNull
     public TestNetworkInterface createTapInterface(boolean carrierUp, boolean bringUp) {
         try {
-            return mService.createInterface(TAP, carrierUp, bringUp, NO_ADDRS, null /* iface */);
+            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.
+     *
+     * @param carrierUp whether the created interface has a carrier or not.
+     * @param bringUp whether to bring up the interface before returning it.
+     * @param disableIpv6ProvisioningDelay whether to disable DAD and RS delay.
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
+    @NonNull
+    public TestNetworkInterface createTapInterface(boolean carrierUp, boolean bringUp,
+            boolean disableIpv6ProvisioningDelay) {
+        try {
+            return mService.createInterface(TAP, carrierUp, bringUp, disableIpv6ProvisioningDelay,
+                    NO_ADDRS, null /* iface */);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * 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();
         }
diff --git a/nearby/OWNERS b/nearby/OWNERS
index 980c221..844ef06 100644
--- a/nearby/OWNERS
+++ b/nearby/OWNERS
@@ -1,4 +1,6 @@
+chenw@google.com
 chunzhang@google.com
 weiwa@google.com
 weiwu@google.com
+xinhe@google.com
 xlythe@google.com
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
index c67821f..6605428 100644
--- a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
@@ -58,13 +58,7 @@
 }
 
 int bpfGetUidStats(uid_t uid, Stats* stats) {
-    BpfMapRO<uint32_t, StatsValue> appUidStatsMap(APP_UID_STATS_MAP_PATH);
-
-    if (!appUidStatsMap.isValid()) {
-        int ret = -errno;
-        ALOGE("Opening appUidStatsMap(%s) failed: %s", APP_UID_STATS_MAP_PATH, strerror(errno));
-        return ret;
-    }
+    static BpfMapRO<uint32_t, StatsValue> appUidStatsMap(APP_UID_STATS_MAP_PATH);
     return bpfGetUidStatsInternal(uid, stats, appUidStatsMap);
 }
 
@@ -100,19 +94,8 @@
 }
 
 int bpfGetIfaceStats(const char* iface, Stats* stats) {
-    BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
-    int ret;
-    if (!ifaceStatsMap.isValid()) {
-        ret = -errno;
-        ALOGE("get ifaceStats map fd failed: %s", strerror(errno));
-        return ret;
-    }
-    BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
-    if (!ifaceIndexNameMap.isValid()) {
-        ret = -errno;
-        ALOGE("get ifaceIndexName map fd failed: %s", strerror(errno));
-        return ret;
-    }
+    static BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
+    static BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
     return bpfGetIfaceStatsInternal(iface, stats, ifaceStatsMap, ifaceIndexNameMap);
 }
 
@@ -186,19 +169,8 @@
 int parseBpfNetworkStatsDetail(std::vector<stats_line>* lines,
                                const std::vector<std::string>& limitIfaces, int limitTag,
                                int limitUid) {
-    BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
-    if (!ifaceIndexNameMap.isValid()) {
-        int ret = -errno;
-        ALOGE("get ifaceIndexName map fd failed: %s", strerror(errno));
-        return ret;
-    }
-
-    BpfMapRO<uint32_t, uint32_t> configurationMap(CONFIGURATION_MAP_PATH);
-    if (!configurationMap.isValid()) {
-        int ret = -errno;
-        ALOGE("get configuration map fd failed: %s", strerror(errno));
-        return ret;
-    }
+    static BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
+    static BpfMapRO<uint32_t, uint32_t> configurationMap(CONFIGURATION_MAP_PATH);
     auto configuration = configurationMap.readValue(CURRENT_STATS_MAP_CONFIGURATION_KEY);
     if (!configuration.ok()) {
         ALOGE("Cannot read the old configuration from map: %s",
@@ -210,12 +182,8 @@
         return -EINVAL;
     }
     const char* statsMapPath = STATS_MAP_PATH[configuration.value()];
+    // TODO: fix this to not constantly reopen the bpf map
     BpfMap<StatsKey, StatsValue> statsMap(statsMapPath);
-    if (!statsMap.isValid()) {
-        int ret = -errno;
-        ALOGE("get stats map fd failed: %s, path: %s", strerror(errno), statsMapPath);
-        return ret;
-    }
 
     // It is safe to read and clear the old map now since the
     // networkStatsFactory should call netd to swap the map in advance already.
@@ -266,20 +234,8 @@
 }
 
 int parseBpfNetworkStatsDev(std::vector<stats_line>* lines) {
-    int ret = 0;
-    BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
-    if (!ifaceIndexNameMap.isValid()) {
-        ret = -errno;
-        ALOGE("get ifaceIndexName map fd failed: %s", strerror(errno));
-        return ret;
-    }
-
-    BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
-    if (!ifaceStatsMap.isValid()) {
-        ret = -errno;
-        ALOGE("get ifaceStats map fd failed: %s", strerror(errno));
-        return ret;
-    }
+    static BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
+    static BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
     return parseBpfNetworkStatsDevInternal(lines, ifaceStatsMap, ifaceIndexNameMap);
 }
 
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 7115720..8818460 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -21,7 +21,6 @@
 
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.PackageManager;
 import android.net.ConnectivityManager;
 import android.net.INetd;
 import android.net.LinkProperties;
@@ -51,6 +50,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
+import com.android.net.module.util.PermissionUtils;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -861,12 +861,7 @@
 
     @Override
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
-        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
-                != PackageManager.PERMISSION_GRANTED) {
-            pw.println("Permission Denial: can't dump " + TAG
-                    + " due to missing android.permission.DUMP permission");
-            return;
-        }
+        if (!PermissionUtils.checkDumpPermission(mContext, TAG, pw)) return;
 
         for (ClientInfo client : mClients.values()) {
             pw.println("Client Info");
diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
index c4ea9ae..e5bddf6 100644
--- a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
+++ b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
@@ -60,7 +60,9 @@
 import java.util.concurrent.ConcurrentHashMap;
 
 /**
- * {@link NetworkProvider} that manages NetworkOffers for Ethernet networks.
+ * Class that manages NetworkOffers for Ethernet networks.
+ *
+ * TODO: this class should be merged into EthernetTracker.
  */
 public class EthernetNetworkFactory {
     private final static String TAG = EthernetNetworkFactory.class.getSimpleName();
@@ -221,11 +223,17 @@
     }
 
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
-    protected void removeInterface(String interfaceName) {
+    protected boolean removeInterface(String interfaceName) {
         NetworkInterfaceState iface = mTrackingInterfaces.remove(interfaceName);
         if (iface != null) {
-            iface.destroy();
+            iface.unregisterNetworkOfferAndStop();
+            return true;
         }
+        // TODO(b/236892130): if an interface is currently in server mode, it may not be properly
+        // removed.
+        // TODO: when false is returned, do not send a STATE_ABSENT callback.
+        Log.w(TAG, interfaceName + " is not tracked and cannot be removed");
+        return false;
     }
 
     /** Returns true if state has been modified */
@@ -285,14 +293,14 @@
         private final Context mContext;
         private final NetworkProvider mNetworkProvider;
         private final Dependencies mDeps;
-        private final NetworkProvider.NetworkOfferCallback mNetworkOfferCallback;
+        private NetworkProvider.NetworkOfferCallback mNetworkOfferCallback;
 
         private static String sTcpBufferSizes = null;  // Lazy initialized.
 
         private boolean mLinkUp;
         private int mLegacyType;
         private LinkProperties mLinkProperties = new LinkProperties();
-        private Set<NetworkRequest> mRequests = new ArraySet<>();
+        private final Set<Integer> mRequestIds = new ArraySet<>();
 
         private volatile @Nullable IpClientManager mIpClient;
         private @NonNull NetworkCapabilities mCapabilities;
@@ -392,8 +400,15 @@
         }
 
         private class EthernetNetworkOfferCallback implements NetworkProvider.NetworkOfferCallback {
+            private boolean isStale() {
+                return this != mNetworkOfferCallback;
+            }
+
             @Override
             public void onNetworkNeeded(@NonNull NetworkRequest request) {
+                if (isStale()) {
+                    return;
+                }
                 if (DBG) {
                     Log.d(TAG, String.format("%s: onNetworkNeeded for request: %s", name, request));
                 }
@@ -401,19 +416,26 @@
                 // existing requests.
                 // ConnectivityService filters requests for us based on the NetworkCapabilities
                 // passed in the registerNetworkOffer() call.
-                mRequests.add(request);
+                mRequestIds.add(request.requestId);
                 // if the network is already started, this is a no-op.
                 start();
             }
 
             @Override
             public void onNetworkUnneeded(@NonNull NetworkRequest request) {
+                if (isStale()) {
+                    return;
+                }
                 if (DBG) {
                     Log.d(TAG,
                             String.format("%s: onNetworkUnneeded for request: %s", name, request));
                 }
-                mRequests.remove(request);
-                if (mRequests.isEmpty()) {
+                if (!mRequestIds.remove(request.requestId)) {
+                    // This can only happen if onNetworkNeeded was not called for a request or if
+                    // the requestId changed. Both should *never* happen.
+                    Log.wtf(TAG, "onNetworkUnneeded called for unknown request");
+                }
+                if (mRequestIds.isEmpty()) {
                     // not currently serving any requests, stop the network.
                     stop();
                 }
@@ -431,7 +453,6 @@
             mContext = context;
             mNetworkProvider = networkProvider;
             mDeps = deps;
-            mNetworkOfferCallback = new EthernetNetworkOfferCallback();
             mHwAddress = hwAddress;
         }
 
@@ -454,7 +475,7 @@
                     + "transport type.");
         }
 
-        private static NetworkScore getBestNetworkScore() {
+        private static NetworkScore getNetworkScore() {
             return new NetworkScore.Builder().build();
         }
 
@@ -465,9 +486,7 @@
             if (mLinkUp) {
                 // registering a new network offer will update the existing one, not install a
                 // new one.
-                mNetworkProvider.registerNetworkOffer(getBestNetworkScore(),
-                        new NetworkCapabilities(capabilities), cmd -> mHandler.post(cmd),
-                        mNetworkOfferCallback);
+                registerNetworkOffer();
             }
         }
 
@@ -629,16 +648,13 @@
 
             if (!up) { // was up, goes down
                 // retract network offer and stop IpClient.
-                destroy();
-                // If only setting the interface down, send a callback to signal completion.
-                EthernetNetworkFactory.maybeSendNetworkManagementCallback(listener, name, null);
+                unregisterNetworkOfferAndStop();
             } else { // was down, goes up
                 // register network offer
-                mNetworkProvider.registerNetworkOffer(getBestNetworkScore(),
-                        new NetworkCapabilities(mCapabilities), (cmd) -> mHandler.post(cmd),
-                        mNetworkOfferCallback);
+                registerNetworkOffer();
             }
 
+            EthernetNetworkFactory.maybeSendNetworkManagementCallback(listener, name, null);
             return true;
         }
 
@@ -660,10 +676,24 @@
             mLinkProperties.clear();
         }
 
-        public void destroy() {
+        private void registerNetworkOffer() {
+            // If mNetworkOfferCallback is already set, it should be reused to update the existing
+            // offer.
+            if (mNetworkOfferCallback == null) {
+                mNetworkOfferCallback = new EthernetNetworkOfferCallback();
+            }
+            mNetworkProvider.registerNetworkOffer(getNetworkScore(),
+                    new NetworkCapabilities(mCapabilities), cmd -> mHandler.post(cmd),
+                    mNetworkOfferCallback);
+        }
+
+        private void unregisterNetworkOfferAndStop() {
             mNetworkProvider.unregisterNetworkOffer(mNetworkOfferCallback);
+            // Setting mNetworkOfferCallback to null allows the callback object to be identified
+            // as stale.
+            mNetworkOfferCallback = null;
             stop();
-            mRequests.clear();
+            mRequestIds.clear();
         }
 
         private static void provisionIpClient(@NonNull final IpClientManager ipClient,
diff --git a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
index f058f94..dae3d2a 100644
--- a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
+++ b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
@@ -32,7 +32,6 @@
 import android.net.IpConfiguration;
 import android.net.NetworkCapabilities;
 import android.net.NetworkSpecifier;
-import android.os.Binder;
 import android.os.Handler;
 import android.os.RemoteException;
 import android.util.Log;
@@ -188,13 +187,7 @@
     @Override
     protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
         final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
-        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
-                != PackageManager.PERMISSION_GRANTED) {
-            pw.println("Permission Denial: can't dump EthernetService from pid="
-                    + Binder.getCallingPid()
-                    + ", uid=" + Binder.getCallingUid());
-            return;
-        }
+        if (!PermissionUtils.checkDumpPermission(mContext, TAG, pw)) return;
 
         pw.println("Current Ethernet state: ");
         pw.increaseIndent();
diff --git a/service-t/src/com/android/server/net/NetworkStatsFactory.java b/service-t/src/com/android/server/net/NetworkStatsFactory.java
index 3b93f1a..b628251 100644
--- a/service-t/src/com/android/server/net/NetworkStatsFactory.java
+++ b/service-t/src/com/android/server/net/NetworkStatsFactory.java
@@ -164,16 +164,17 @@
     }
 
     public NetworkStatsFactory(@NonNull Context ctx) {
-        this(ctx, new File("/proc/"), true);
+        this(ctx, new File("/proc/"), true, new BpfNetMaps());
     }
 
     @VisibleForTesting
-    public NetworkStatsFactory(@NonNull Context ctx, File procRoot, boolean useBpfStats) {
+    public NetworkStatsFactory(@NonNull Context ctx, File procRoot, boolean useBpfStats,
+            BpfNetMaps bpfNetMaps) {
         mStatsXtIfaceAll = new File(procRoot, "net/xt_qtaguid/iface_stat_all");
         mStatsXtIfaceFmt = new File(procRoot, "net/xt_qtaguid/iface_stat_fmt");
         mStatsXtUid = new File(procRoot, "net/xt_qtaguid/stats");
         mUseBpfStats = useBpfStats;
-        mBpfNetMaps = new BpfNetMaps();
+        mBpfNetMaps = bpfNetMaps;
         synchronized (mPersistentDataLock) {
             mPersistSnapshot = new NetworkStats(SystemClock.elapsedRealtime(), -1);
             mTunAnd464xlatAdjustedStats = new NetworkStats(SystemClock.elapsedRealtime(), -1);
diff --git a/service-t/src/com/android/server/net/NetworkStatsRecorder.java b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
index 7c801d7..3da1585 100644
--- a/service-t/src/com/android/server/net/NetworkStatsRecorder.java
+++ b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
@@ -541,7 +541,8 @@
 
     /**
      * Recover from {@link FileRotator} failure by dumping state to
-     * {@link DropBoxManager} and deleting contents.
+     * {@link DropBoxManager} and deleting contents if this recorder
+     * sets {@code mWipeOnError} to true, otherwise keep the contents.
      */
     void recoverAndDeleteData() {
         if (DUMP_BEFORE_DELETE) {
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 424dcd9..08d2a3c 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -24,7 +24,6 @@
 import static android.content.Intent.ACTION_UID_REMOVED;
 import static android.content.Intent.ACTION_USER_REMOVED;
 import static android.content.Intent.EXTRA_UID;
-import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.NetworkStats.DEFAULT_NETWORK_ALL;
@@ -304,7 +303,7 @@
         /**
          * When enabled, all mobile data is reported under {@link NetworkTemplate#NETWORK_TYPE_ALL}.
          * When disabled, mobile data is broken down by a granular ratType representative of the
-         * actual ratType. {@see android.app.usage.NetworkStatsManager#getCollapsedRatType}.
+         * actual ratType. See {@link android.app.usage.NetworkStatsManager#getCollapsedRatType}.
          * Enabling this decreases the level of detail but saves performance, disk space and
          * amount of data logged.
          */
@@ -2810,30 +2809,12 @@
                     throw new IllegalStateException("invalid tethering stats " + e);
                 }
             }
-        } catch (IllegalStateException e) {
+        } catch (IllegalStateException | ServiceSpecificException e) {
             Log.wtf(TAG, "problem reading network stats", e);
         }
         return stats;
     }
 
-    // TODO: It is copied from ConnectivityService, consider refactor these check permission
-    //  functions to a proper util.
-    private boolean checkAnyPermissionOf(String... permissions) {
-        for (String permission : permissions) {
-            if (mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    private void enforceAnyPermissionOf(String... permissions) {
-        if (!checkAnyPermissionOf(permissions)) {
-            throw new SecurityException("Requires one of the following permissions: "
-                    + String.join(", ", permissions) + ".");
-        }
-    }
-
     /**
      * Registers a custom provider of {@link android.net.NetworkStats} to combine the network
      * statistics that cannot be seen by the kernel to system. To unregister, invoke the
@@ -2848,7 +2829,7 @@
      */
     public @NonNull INetworkStatsProviderCallback registerNetworkStatsProvider(
             @NonNull String tag, @NonNull INetworkStatsProvider provider) {
-        enforceAnyPermissionOf(NETWORK_STATS_PROVIDER,
+        PermissionUtils.enforceAnyPermissionOf(mContext, NETWORK_STATS_PROVIDER,
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
         Objects.requireNonNull(provider, "provider is null");
         Objects.requireNonNull(tag, "tag is null");
diff --git a/service/Android.bp b/service/Android.bp
index c2dbce1..499af25 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -158,6 +158,7 @@
     static_libs: [
         // Do not add libs here if they are already included
         // in framework-connectivity
+        "connectivity-net-module-utils-bpf",
         "connectivity_native_aidl_interface-lateststable-java",
         "dnsresolver_aidl_interface-V9-java",
         "modules-utils-shell-command-handler",
@@ -260,9 +261,15 @@
     installable: true,
 }
 
-filegroup {
+genrule {
     name: "connectivity-jarjar-rules",
-    srcs: ["jarjar-rules.txt"],
+    defaults: ["jarjar-rules-combine-defaults"],
+    srcs: [
+        ":framework-connectivity-jarjar-rules",
+        ":service-connectivity-jarjar-gen",
+        ":service-nearby-jarjar-gen",
+    ],
+    out: ["connectivity-jarjar-rules.txt"],
     visibility: ["//packages/modules/Connectivity:__subpackages__"],
 }
 
@@ -273,3 +280,41 @@
     srcs: ["src/com/android/server/BpfNetMaps.java"],
     visibility: ["//packages/modules/Connectivity:__subpackages__"],
 }
+
+java_genrule {
+    name: "service-connectivity-jarjar-gen",
+    tool_files: [
+        ":service-connectivity-pre-jarjar{.jar}",
+        ":service-connectivity-tiramisu-pre-jarjar{.jar}",
+        "jarjar-excludes.txt",
+    ],
+    tools: [
+        "jarjar-rules-generator",
+    ],
+    out: ["service_connectivity_jarjar_rules.txt"],
+    cmd: "$(location jarjar-rules-generator) " +
+        "--jars $(location :service-connectivity-pre-jarjar{.jar}) " +
+        "$(location :service-connectivity-tiramisu-pre-jarjar{.jar}) " +
+        "--prefix android.net.connectivity " +
+        "--excludes $(location jarjar-excludes.txt) " +
+        "--output $(out)",
+    visibility: ["//visibility:private"],
+}
+
+java_genrule {
+    name: "service-nearby-jarjar-gen",
+    tool_files: [
+        ":service-nearby-pre-jarjar{.jar}",
+        "jarjar-excludes.txt",
+    ],
+    tools: [
+        "jarjar-rules-generator",
+    ],
+    out: ["service_nearby_jarjar_rules.txt"],
+    cmd: "$(location jarjar-rules-generator) " +
+        "--jars $(location :service-nearby-pre-jarjar{.jar}) " +
+        "--prefix com.android.server.nearby " +
+        "--excludes $(location jarjar-excludes.txt) " +
+        "--output $(out)",
+    visibility: ["//visibility:private"],
+}
diff --git a/service/jarjar-rules.txt b/service/jarjar-rules.txt
deleted file mode 100644
index 4013d2e..0000000
--- a/service/jarjar-rules.txt
+++ /dev/null
@@ -1,124 +0,0 @@
-# Classes in framework-connectivity are restricted to the android.net package.
-# This cannot be changed because it is harcoded in ART in S.
-# Any missing jarjar rule for framework-connectivity would be caught by the
-# build as an unexpected class outside of the android.net package.
-rule com.android.net.module.util.** android.net.connectivity.@0
-rule com.android.modules.utils.** android.net.connectivity.@0
-rule android.net.NetworkFactory* android.net.connectivity.@0
-
-# From modules-utils-preconditions
-rule com.android.internal.util.Preconditions* android.net.connectivity.@0
-
-# From framework-connectivity-shared-srcs
-rule android.util.LocalLog* android.net.connectivity.@0
-rule android.util.IndentingPrintWriter* android.net.connectivity.@0
-rule com.android.internal.util.IndentingPrintWriter* android.net.connectivity.@0
-rule com.android.internal.util.MessageUtils* android.net.connectivity.@0
-rule com.android.internal.util.WakeupMessage* android.net.connectivity.@0
-rule com.android.internal.util.FileRotator* android.net.connectivity.@0
-rule com.android.internal.util.ProcFileReader* android.net.connectivity.@0
-
-# From framework-connectivity-protos
-rule com.google.protobuf.** android.net.connectivity.@0
-rule android.service.** android.net.connectivity.@0
-
-rule android.sysprop.** com.android.connectivity.@0
-
-rule com.android.internal.messages.** com.android.connectivity.@0
-
-# From dnsresolver_aidl_interface (newer AIDLs should go to android.net.resolv.aidl)
-rule android.net.resolv.aidl.** com.android.connectivity.@0
-rule android.net.IDnsResolver* com.android.connectivity.@0
-rule android.net.ResolverHostsParcel* com.android.connectivity.@0
-rule android.net.ResolverOptionsParcel* com.android.connectivity.@0
-rule android.net.ResolverParamsParcel* com.android.connectivity.@0
-rule android.net.ResolverParamsParcel* com.android.connectivity.@0
-# Also includes netd event listener AIDL, but this is handled by netd-client rules
-
-# From netd-client (newer AIDLs should go to android.net.netd.aidl)
-rule android.net.netd.aidl.** com.android.connectivity.@0
-# Avoid including android.net.INetdEventCallback, used in tests but not part of the module
-rule android.net.INetd com.android.connectivity.@0
-rule android.net.INetd$* com.android.connectivity.@0
-rule android.net.INetdUnsolicitedEventListener* com.android.connectivity.@0
-rule android.net.InterfaceConfigurationParcel* com.android.connectivity.@0
-rule android.net.MarkMaskParcel* com.android.connectivity.@0
-rule android.net.NativeNetworkConfig* com.android.connectivity.@0
-rule android.net.NativeNetworkType* com.android.connectivity.@0
-rule android.net.NativeVpnType* com.android.connectivity.@0
-rule android.net.RouteInfoParcel* com.android.connectivity.@0
-rule android.net.TetherConfigParcel* com.android.connectivity.@0
-rule android.net.TetherOffloadRuleParcel* com.android.connectivity.@0
-rule android.net.TetherStatsParcel* com.android.connectivity.@0
-rule android.net.UidRangeParcel* com.android.connectivity.@0
-rule android.net.metrics.INetdEventListener* com.android.connectivity.@0
-
-# From netlink-client
-rule android.net.netlink.** com.android.connectivity.@0
-
-# From networkstack-client (newer AIDLs should go to android.net.[networkstack|ipmemorystore].aidl)
-rule android.net.networkstack.aidl.** com.android.connectivity.@0
-rule android.net.ipmemorystore.aidl.** com.android.connectivity.@0
-rule android.net.ipmemorystore.aidl.** com.android.connectivity.@0
-rule android.net.DataStallReportParcelable* com.android.connectivity.@0
-rule android.net.DhcpResultsParcelable* com.android.connectivity.@0
-rule android.net.IIpMemoryStore* com.android.connectivity.@0
-rule android.net.INetworkMonitor* com.android.connectivity.@0
-rule android.net.INetworkStackConnector* com.android.connectivity.@0
-rule android.net.INetworkStackStatusCallback* com.android.connectivity.@0
-rule android.net.InformationElementParcelable* com.android.connectivity.@0
-rule android.net.InitialConfigurationParcelable* com.android.connectivity.@0
-rule android.net.IpMemoryStore* com.android.connectivity.@0
-rule android.net.Layer2InformationParcelable* com.android.connectivity.@0
-rule android.net.Layer2PacketParcelable* com.android.connectivity.@0
-rule android.net.NattKeepalivePacketDataParcelable* com.android.connectivity.@0
-rule android.net.NetworkMonitorManager* com.android.connectivity.@0
-rule android.net.NetworkTestResultParcelable* com.android.connectivity.@0
-rule android.net.PrivateDnsConfigParcel* com.android.connectivity.@0
-rule android.net.ProvisioningConfigurationParcelable* com.android.connectivity.@0
-rule android.net.ScanResultInfoParcelable* com.android.connectivity.@0
-rule android.net.TcpKeepalivePacketDataParcelable* com.android.connectivity.@0
-rule android.net.dhcp.DhcpLeaseParcelable* com.android.connectivity.@0
-rule android.net.dhcp.DhcpServingParamsParcel* com.android.connectivity.@0
-rule android.net.dhcp.IDhcpEventCallbacks* com.android.connectivity.@0
-rule android.net.dhcp.IDhcpServer* com.android.connectivity.@0
-rule android.net.ip.IIpClient* com.android.connectivity.@0
-rule android.net.ip.IpClientCallbacks* com.android.connectivity.@0
-rule android.net.ip.IpClientManager* com.android.connectivity.@0
-rule android.net.ip.IpClientUtil* com.android.connectivity.@0
-rule android.net.ipmemorystore.** com.android.connectivity.@0
-rule android.net.networkstack.** com.android.connectivity.@0
-rule android.net.shared.** com.android.connectivity.@0
-rule android.net.util.KeepalivePacketDataUtil* com.android.connectivity.@0
-
-# From connectivity-module-utils
-rule android.net.util.SharedLog* com.android.connectivity.@0
-rule android.net.shared.** com.android.connectivity.@0
-
-# From services-connectivity-shared-srcs
-rule android.net.util.NetworkConstants* com.android.connectivity.@0
-
-# From modules-utils-statemachine
-rule com.android.internal.util.IState* com.android.connectivity.@0
-rule com.android.internal.util.State* com.android.connectivity.@0
-
-# From the API shims
-rule com.android.networkstack.apishim.** com.android.connectivity.@0
-
-# From filegroup framework-connectivity-protos
-rule android.service.*Proto com.android.connectivity.@0
-
-# From mdns-aidl-interface
-rule android.net.mdns.aidl.** android.net.connectivity.@0
-
-# From nearby-service, including proto
-rule service.proto.** com.android.server.nearby.@0
-rule androidx.annotation.Keep* com.android.server.nearby.@0
-rule androidx.collection.** com.android.server.nearby.@0
-rule androidx.core.** com.android.server.nearby.@0
-rule androidx.versionedparcelable.** com.android.server.nearby.@0
-rule com.google.common.** com.android.server.nearby.@0
-rule android.support.v4.** com.android.server.nearby.@0
-
-# Remaining are connectivity sources in com.android.server and com.android.server.connectivity:
-# TODO: move to a subpackage of com.android.connectivity (such as com.android.connectivity.server)
diff --git a/service/jni/com_android_server_TestNetworkService.cpp b/service/jni/com_android_server_TestNetworkService.cpp
index 9c7a761..a1d0310 100644
--- a/service/jni/com_android_server_TestNetworkService.cpp
+++ b/service/jni/com_android_server_TestNetworkService.cpp
@@ -76,27 +76,47 @@
         setTunTapCarrierEnabledImpl(env, iface, tun.get(), hasCarrier);
     }
 
-    // Activate interface using an unconnected datagram socket.
-    base::unique_fd inet6CtrlSock(socket(AF_INET6, SOCK_DGRAM, 0));
-    ifr.ifr_flags = IFF_UP;
     // Mark TAP interfaces as supporting multicast
-    if (!isTun) ifr.ifr_flags |= IFF_MULTICAST;
+    if (!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, "activating", ifr.ifr_name);
-        return -1;
+        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);
 }
@@ -112,11 +132,21 @@
     return createTunTapImpl(env, isTun, hasCarrier, 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", "(ZZLjava/lang/String;)I", (void*)createTunTap},
+    {"nativeBringUpInterface", "(Ljava/lang/String;)V", (void*)bringUpInterface},
 };
 
 int register_com_android_server_TestNetworkService(JNIEnv* env) {
diff --git a/service/mdns/com/android/server/connectivity/mdns/util/MdnsLogger.java b/service/mdns/com/android/server/connectivity/mdns/util/MdnsLogger.java
index 31c62f5..431f1fd 100644
--- a/service/mdns/com/android/server/connectivity/mdns/util/MdnsLogger.java
+++ b/service/mdns/com/android/server/connectivity/mdns/util/MdnsLogger.java
@@ -16,9 +16,10 @@
 
 package com.android.server.connectivity.mdns.util;
 
-import android.net.util.SharedLog;
 import android.text.TextUtils;
 
+import com.android.net.module.util.SharedLog;
+
 /**
  * The logger used in mDNS.
  */
@@ -58,4 +59,4 @@
     public void w(String message) {
         mLog.w(message);
     }
-}
\ No newline at end of file
+}
diff --git a/service/proguard.flags b/service/proguard.flags
index cffa490..f546e82 100644
--- a/service/proguard.flags
+++ b/service/proguard.flags
@@ -2,8 +2,6 @@
 # TODO: instead of keeping everything, consider listing only "entry points"
 # (service loader, JNI registered methods, etc) and letting the optimizer do its job
 -keep class android.net.** { *; }
--keep class com.android.connectivity.** { *; }
--keep class com.android.net.** { *; }
 -keep class !com.android.server.nearby.**,com.android.server.** { *; }
 
 # Prevent proguard from stripping out any nearby-service and fast-pair-lite-protos fields.
@@ -13,5 +11,5 @@
 # the schema, keep all the fields.
 # This replicates the base proguard rule used by the build by default
 # (proguard_basic_keeps.flags), but needs to be specified here because the
-# com.google.protobuf package is jarjared to the below package.
--keepclassmembers class * extends android.net.connectivity.com.google.protobuf.MessageLite { <fields>; }
+# com.google.protobuf package is jarjared to use a package prefix.
+-keepclassmembers class * extends **.com.google.protobuf.MessageLite { <fields>; }
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index 3ee3ea1..d7c5a06 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -24,7 +24,10 @@
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_POWERSAVE;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_RESTRICTED;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_STANDBY;
+import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
+import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
 import static android.system.OsConstants.EINVAL;
+import static android.system.OsConstants.ENODEV;
 import static android.system.OsConstants.ENOENT;
 import static android.system.OsConstants.EOPNOTSUPP;
 
@@ -34,8 +37,8 @@
 import android.system.ErrnoException;
 import android.system.Os;
 import android.util.Log;
-import android.util.SparseLongArray;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.BpfMap;
@@ -50,10 +53,17 @@
  * {@hide}
  */
 public class BpfNetMaps {
+    private static final boolean PRE_T = !SdkLevel.isAtLeastT();
+    static {
+        if (!PRE_T) {
+            System.loadLibrary("service-connectivity");
+        }
+    }
+
     private static final String TAG = "BpfNetMaps";
     private final INetd mNetd;
+    private final Dependencies mDeps;
     // Use legacy netd for releases before T.
-    private static final boolean USE_NETD = !SdkLevel.isAtLeastT();
     private static boolean sInitialized = false;
 
     // Lock for sConfigurationMap entry for UID_RULES_CONFIGURATION_KEY.
@@ -63,47 +73,70 @@
 
     private static final String CONFIGURATION_MAP_PATH =
             "/sys/fs/bpf/netd_shared/map_netd_configuration_map";
+    private static final String UID_OWNER_MAP_PATH =
+            "/sys/fs/bpf/netd_shared/map_netd_uid_owner_map";
     private static final U32 UID_RULES_CONFIGURATION_KEY = new U32(0);
     private static BpfMap<U32, U32> sConfigurationMap = null;
+    // BpfMap for UID_OWNER_MAP_PATH. This map is not accessed by others.
+    private static BpfMap<U32, UidOwnerValue> sUidOwnerMap = null;
 
     // LINT.IfChange(match_type)
-    private static final long NO_MATCH = 0;
-    private static final long HAPPY_BOX_MATCH = (1 << 0);
-    private static final long PENALTY_BOX_MATCH = (1 << 1);
-    private static final long DOZABLE_MATCH = (1 << 2);
-    private static final long STANDBY_MATCH = (1 << 3);
-    private static final long POWERSAVE_MATCH = (1 << 4);
-    private static final long RESTRICTED_MATCH = (1 << 5);
-    private static final long LOW_POWER_STANDBY_MATCH = (1 << 6);
-    private static final long IIF_MATCH = (1 << 7);
-    private static final long LOCKDOWN_VPN_MATCH = (1 << 8);
-    private static final long OEM_DENY_1_MATCH = (1 << 9);
-    private static final long OEM_DENY_2_MATCH = (1 << 10);
-    private static final long OEM_DENY_3_MATCH = (1 << 11);
+    @VisibleForTesting public static final long NO_MATCH = 0;
+    @VisibleForTesting public static final long HAPPY_BOX_MATCH = (1 << 0);
+    @VisibleForTesting public static final long PENALTY_BOX_MATCH = (1 << 1);
+    @VisibleForTesting public static final long DOZABLE_MATCH = (1 << 2);
+    @VisibleForTesting public static final long STANDBY_MATCH = (1 << 3);
+    @VisibleForTesting public static final long POWERSAVE_MATCH = (1 << 4);
+    @VisibleForTesting public static final long RESTRICTED_MATCH = (1 << 5);
+    @VisibleForTesting public static final long LOW_POWER_STANDBY_MATCH = (1 << 6);
+    @VisibleForTesting public static final long IIF_MATCH = (1 << 7);
+    @VisibleForTesting public static final long LOCKDOWN_VPN_MATCH = (1 << 8);
+    @VisibleForTesting public static final long OEM_DENY_1_MATCH = (1 << 9);
+    @VisibleForTesting public static final long OEM_DENY_2_MATCH = (1 << 10);
+    @VisibleForTesting public static final long OEM_DENY_3_MATCH = (1 << 11);
     // LINT.ThenChange(packages/modules/Connectivity/bpf_progs/bpf_shared.h)
 
-    // TODO: Use Java BpfMap instead of JNI code (TrafficController) for map update.
-    // Currently, BpfNetMaps uses TrafficController for map update and TrafficController
-    // (changeUidOwnerRule and toggleUidOwnerMap) also does conversion from "firewall chain" to
-    // "match". Migrating map update from JNI to Java BpfMap will solve this duplication.
-    private static final SparseLongArray FIREWALL_CHAIN_TO_MATCH = new SparseLongArray();
-    static {
-        FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_DOZABLE, DOZABLE_MATCH);
-        FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_STANDBY, STANDBY_MATCH);
-        FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_POWERSAVE, POWERSAVE_MATCH);
-        FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_RESTRICTED, RESTRICTED_MATCH);
-        FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_LOW_POWER_STANDBY, LOW_POWER_STANDBY_MATCH);
-        FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_OEM_DENY_1, OEM_DENY_1_MATCH);
-        FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_OEM_DENY_2, OEM_DENY_2_MATCH);
-        FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_OEM_DENY_3, OEM_DENY_3_MATCH);
+    /**
+     * Set configurationMap for test.
+     */
+    @VisibleForTesting
+    public static void setConfigurationMapForTest(BpfMap<U32, U32> configurationMap) {
+        sConfigurationMap = configurationMap;
     }
 
     /**
-     * Only tests or BpfNetMaps#ensureInitialized can call this function.
+     * Set uidOwnerMap for test.
      */
     @VisibleForTesting
-    public static void initialize(final Dependencies deps) {
-        sConfigurationMap = deps.getConfigurationMap();
+    public static void setUidOwnerMapForTest(BpfMap<U32, UidOwnerValue> uidOwnerMap) {
+        sUidOwnerMap = uidOwnerMap;
+    }
+
+    private static BpfMap<U32, U32> getConfigurationMap() {
+        try {
+            return new BpfMap<>(
+                    CONFIGURATION_MAP_PATH, BpfMap.BPF_F_RDWR, U32.class, U32.class);
+        } catch (ErrnoException e) {
+            throw new IllegalStateException("Cannot open netd configuration map", e);
+        }
+    }
+
+    private static BpfMap<U32, UidOwnerValue> getUidOwnerMap() {
+        try {
+            return new BpfMap<>(
+                    UID_OWNER_MAP_PATH, BpfMap.BPF_F_RDWR, U32.class, UidOwnerValue.class);
+        } catch (ErrnoException e) {
+            throw new IllegalStateException("Cannot open uid owner map", e);
+        }
+    }
+
+    private static void setBpfMaps() {
+        if (sConfigurationMap == null) {
+            sConfigurationMap = getConfigurationMap();
+        }
+        if (sUidOwnerMap == null) {
+            sUidOwnerMap = getUidOwnerMap();
+        }
     }
 
     /**
@@ -112,11 +145,8 @@
      */
     private static synchronized void ensureInitialized() {
         if (sInitialized) return;
-        if (!USE_NETD) {
-            System.loadLibrary("service-connectivity");
-            native_init();
-            initialize(new Dependencies());
-        }
+        setBpfMaps();
+        native_init();
         sInitialized = true;
     }
 
@@ -126,16 +156,10 @@
     @VisibleForTesting
     public static class Dependencies {
         /**
-         *  Get configuration BPF map.
+         * Get interface index.
          */
-        public BpfMap<U32, U32> getConfigurationMap() {
-            try {
-                return new BpfMap<>(
-                        CONFIGURATION_MAP_PATH, BpfMap.BPF_F_RDWR, U32.class, U32.class);
-            } catch (ErrnoException e) {
-                Log.e(TAG, "Cannot open netd configuration map: " + e);
-                return null;
-            }
+        public int getIfIndex(final String ifName) {
+            return Os.if_nametoindex(ifName);
         }
     }
 
@@ -143,12 +167,20 @@
     public BpfNetMaps() {
         this(null);
 
-        if (USE_NETD) throw new IllegalArgumentException("BpfNetMaps need to use netd before T");
+        if (PRE_T) throw new IllegalArgumentException("BpfNetMaps need to use netd before T");
     }
 
     public BpfNetMaps(final INetd netd) {
-        ensureInitialized();
+        this(netd, new Dependencies());
+    }
+
+    @VisibleForTesting
+    public BpfNetMaps(final INetd netd, final Dependencies deps) {
+        if (!PRE_T) {
+            ensureInitialized();
+        }
         mNetd = netd;
+        mDeps = deps;
     }
 
     /**
@@ -156,11 +188,50 @@
      */
     @VisibleForTesting
     public long getMatchByFirewallChain(final int chain) {
-        final long match = FIREWALL_CHAIN_TO_MATCH.get(chain, NO_MATCH);
-        if (match == NO_MATCH) {
-            throw new ServiceSpecificException(EINVAL, "Invalid firewall chain: " + chain);
+        switch (chain) {
+            case FIREWALL_CHAIN_DOZABLE:
+                return DOZABLE_MATCH;
+            case FIREWALL_CHAIN_STANDBY:
+                return STANDBY_MATCH;
+            case FIREWALL_CHAIN_POWERSAVE:
+                return POWERSAVE_MATCH;
+            case FIREWALL_CHAIN_RESTRICTED:
+                return RESTRICTED_MATCH;
+            case FIREWALL_CHAIN_LOW_POWER_STANDBY:
+                return LOW_POWER_STANDBY_MATCH;
+            case FIREWALL_CHAIN_OEM_DENY_1:
+                return OEM_DENY_1_MATCH;
+            case FIREWALL_CHAIN_OEM_DENY_2:
+                return OEM_DENY_2_MATCH;
+            case FIREWALL_CHAIN_OEM_DENY_3:
+                return OEM_DENY_3_MATCH;
+            default:
+                throw new ServiceSpecificException(EINVAL, "Invalid firewall chain: " + chain);
         }
-        return match;
+    }
+
+    /**
+     * Get if the chain is allow list or not.
+     *
+     * ALLOWLIST means the firewall denies all by default, uids must be explicitly allowed
+     * DENYLIST means the firewall allows all by default, uids must be explicitly denyed
+     */
+    @VisibleForTesting
+    public boolean isFirewallAllowList(final int chain) {
+        switch (chain) {
+            case FIREWALL_CHAIN_DOZABLE:
+            case FIREWALL_CHAIN_POWERSAVE:
+            case FIREWALL_CHAIN_RESTRICTED:
+            case FIREWALL_CHAIN_LOW_POWER_STANDBY:
+                return true;
+            case FIREWALL_CHAIN_STANDBY:
+            case FIREWALL_CHAIN_OEM_DENY_1:
+            case FIREWALL_CHAIN_OEM_DENY_2:
+            case FIREWALL_CHAIN_OEM_DENY_3:
+                return false;
+            default:
+                throw new ServiceSpecificException(EINVAL, "Invalid firewall chain: " + chain);
+        }
     }
 
     private void maybeThrow(final int err, final String msg) {
@@ -169,12 +240,73 @@
         }
     }
 
-    private void throwIfUseNetd(final String msg) {
-        if (USE_NETD) {
+    private void throwIfPreT(final String msg) {
+        if (PRE_T) {
             throw new UnsupportedOperationException(msg);
         }
     }
 
+    private void removeRule(final int uid, final long match, final String caller) {
+        try {
+            synchronized (sUidOwnerMap) {
+                final UidOwnerValue oldMatch = sUidOwnerMap.getValue(new U32(uid));
+
+                if (oldMatch == null) {
+                    throw new ServiceSpecificException(ENOENT,
+                            "sUidOwnerMap does not have entry for uid: " + uid);
+                }
+
+                final UidOwnerValue newMatch = new UidOwnerValue(
+                        (match == IIF_MATCH) ? 0 : oldMatch.iif,
+                        oldMatch.rule & ~match
+                );
+
+                if (newMatch.rule == 0) {
+                    sUidOwnerMap.deleteEntry(new U32(uid));
+                } else {
+                    sUidOwnerMap.updateEntry(new U32(uid), newMatch);
+                }
+            }
+        } catch (ErrnoException e) {
+            throw new ServiceSpecificException(e.errno,
+                    caller + " failed to remove rule: " + Os.strerror(e.errno));
+        }
+    }
+
+    private void addRule(final int uid, final long match, final long iif, final String caller) {
+        if (match != IIF_MATCH && iif != 0) {
+            throw new ServiceSpecificException(EINVAL,
+                    "Non-interface match must have zero interface index");
+        }
+
+        try {
+            synchronized (sUidOwnerMap) {
+                final UidOwnerValue oldMatch = sUidOwnerMap.getValue(new U32(uid));
+
+                final UidOwnerValue newMatch;
+                if (oldMatch != null) {
+                    newMatch = new UidOwnerValue(
+                            (match == IIF_MATCH) ? iif : oldMatch.iif,
+                            oldMatch.rule | match
+                    );
+                } else {
+                    newMatch = new UidOwnerValue(
+                            iif,
+                            match
+                    );
+                }
+                sUidOwnerMap.updateEntry(new U32(uid), newMatch);
+            }
+        } catch (ErrnoException e) {
+            throw new ServiceSpecificException(e.errno,
+                    caller + " failed to add rule: " + Os.strerror(e.errno));
+        }
+    }
+
+    private void addRule(final int uid, final long match, final String caller) {
+        addRule(uid, match, 0 /* iif */, caller);
+    }
+
     /**
      * Add naughty app bandwidth rule for specific app
      *
@@ -183,8 +315,8 @@
      *                                  cause of the failure.
      */
     public void addNaughtyApp(final int uid) {
-        final int err = native_addNaughtyApp(uid);
-        maybeThrow(err, "Unable to add naughty app");
+        throwIfPreT("addNaughtyApp is not available on pre-T devices");
+        addRule(uid, PENALTY_BOX_MATCH, "addNaughtyApp");
     }
 
     /**
@@ -195,8 +327,8 @@
      *                                  cause of the failure.
      */
     public void removeNaughtyApp(final int uid) {
-        final int err = native_removeNaughtyApp(uid);
-        maybeThrow(err, "Unable to remove naughty app");
+        throwIfPreT("removeNaughtyApp is not available on pre-T devices");
+        removeRule(uid, PENALTY_BOX_MATCH, "removeNaughtyApp");
     }
 
     /**
@@ -207,8 +339,8 @@
      *                                  cause of the failure.
      */
     public void addNiceApp(final int uid) {
-        final int err = native_addNiceApp(uid);
-        maybeThrow(err, "Unable to add nice app");
+        throwIfPreT("addNiceApp is not available on pre-T devices");
+        addRule(uid, HAPPY_BOX_MATCH, "addNiceApp");
     }
 
     /**
@@ -219,8 +351,8 @@
      *                                  cause of the failure.
      */
     public void removeNiceApp(final int uid) {
-        final int err = native_removeNiceApp(uid);
-        maybeThrow(err, "Unable to remove nice app");
+        throwIfPreT("removeNiceApp is not available on pre-T devices");
+        removeRule(uid, HAPPY_BOX_MATCH, "removeNiceApp");
     }
 
     /**
@@ -233,18 +365,13 @@
      *                                  cause of the failure.
      */
     public void setChildChain(final int childChain, final boolean enable) {
-        throwIfUseNetd("setChildChain is not available on pre-T devices");
+        throwIfPreT("setChildChain is not available on pre-T devices");
 
         final long match = getMatchByFirewallChain(childChain);
         try {
             synchronized (sUidRulesConfigBpfMapLock) {
                 final U32 config = sConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY);
-                if (config == null) {
-                    throw new ServiceSpecificException(ENOENT,
-                            "Unable to get firewall chain status: sConfigurationMap does not have"
-                                    + " entry for UID_RULES_CONFIGURATION_KEY");
-                }
-                final long newConfig = enable ? (config.val | match) : (config.val & (~match));
+                final long newConfig = enable ? (config.val | match) : (config.val & ~match);
                 sConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(newConfig));
             }
         } catch (ErrnoException e) {
@@ -254,7 +381,7 @@
     }
 
     /**
-     * Get the specified firewall chain status.
+     * Get the specified firewall chain's status.
      *
      * @param childChain target chain
      * @return {@code true} if chain is enabled, {@code false} if chain is not enabled.
@@ -262,17 +389,12 @@
      * @throws ServiceSpecificException in case of failure, with an error code indicating the
      *                                  cause of the failure.
      */
-    public boolean getChainEnabled(final int childChain) {
-        throwIfUseNetd("getChainEnabled is not available on pre-T devices");
+    public boolean isChainEnabled(final int childChain) {
+        throwIfPreT("isChainEnabled is not available on pre-T devices");
 
         final long match = getMatchByFirewallChain(childChain);
         try {
             final U32 config = sConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY);
-            if (config == null) {
-                throw new ServiceSpecificException(ENOENT,
-                        "Unable to get firewall chain status: sConfigurationMap does not have"
-                                + " entry for UID_RULES_CONFIGURATION_KEY");
-            }
             return (config.val & match) != 0;
         } catch (ErrnoException e) {
             throw new ServiceSpecificException(e.errno,
@@ -295,11 +417,13 @@
      */
     public int replaceUidChain(final String chainName, final boolean isAllowlist,
             final int[] uids) {
-        final int err = native_replaceUidChain(chainName, isAllowlist, uids);
-        if (err != 0) {
-            Log.e(TAG, "replaceUidChain failed: " + Os.strerror(-err));
+        synchronized (sUidOwnerMap) {
+            final int err = native_replaceUidChain(chainName, isAllowlist, uids);
+            if (err != 0) {
+                Log.e(TAG, "replaceUidChain failed: " + Os.strerror(-err));
+            }
+            return -err;
         }
-        return -err;
     }
 
     /**
@@ -312,8 +436,18 @@
      *                                  cause of the failure.
      */
     public void setUidRule(final int childChain, final int uid, final int firewallRule) {
-        final int err = native_setUidRule(childChain, uid, firewallRule);
-        maybeThrow(err, "Unable to set uid rule");
+        throwIfPreT("setUidRule is not available on pre-T devices");
+
+        final long match = getMatchByFirewallChain(childChain);
+        final boolean isAllowList = isFirewallAllowList(childChain);
+        final boolean add = (firewallRule == FIREWALL_RULE_ALLOW && isAllowList)
+                || (firewallRule == FIREWALL_RULE_DENY && !isAllowList);
+
+        if (add) {
+            addRule(uid, match, "setUidRule");
+        } else {
+            removeRule(uid, match, "setUidRule");
+        }
     }
 
     /**
@@ -334,12 +468,29 @@
      *                                  cause of the failure.
      */
     public void addUidInterfaceRules(final String ifName, final int[] uids) throws RemoteException {
-        if (USE_NETD) {
+        if (PRE_T) {
             mNetd.firewallAddUidInterfaceRules(ifName, uids);
             return;
         }
-        final int err = native_addUidInterfaceRules(ifName, uids);
-        maybeThrow(err, "Unable to add uid interface rules");
+        // Null ifName is a wildcard to allow apps to receive packets on all interfaces and ifIndex
+        // is set to 0.
+        final int ifIndex;
+        if (ifName == null) {
+            ifIndex = 0;
+        } else {
+            ifIndex = mDeps.getIfIndex(ifName);
+            if (ifIndex == 0) {
+                throw new ServiceSpecificException(ENODEV,
+                        "Failed to get index of interface " + ifName);
+            }
+        }
+        for (final int uid: uids) {
+            try {
+                addRule(uid, IIF_MATCH, ifIndex, "addUidInterfaceRules");
+            } catch (ServiceSpecificException e) {
+                Log.e(TAG, "addRule failed uid=" + uid + " ifName=" + ifName + ", " + e);
+            }
+        }
     }
 
     /**
@@ -354,12 +505,17 @@
      *                                  cause of the failure.
      */
     public void removeUidInterfaceRules(final int[] uids) throws RemoteException {
-        if (USE_NETD) {
+        if (PRE_T) {
             mNetd.firewallRemoveUidInterfaceRules(uids);
             return;
         }
-        final int err = native_removeUidInterfaceRules(uids);
-        maybeThrow(err, "Unable to remove uid interface rules");
+        for (final int uid: uids) {
+            try {
+                removeRule(uid, IIF_MATCH, "removeUidInterfaceRules");
+            } catch (ServiceSpecificException e) {
+                Log.e(TAG, "removeRule failed uid=" + uid + ", " + e);
+            }
+        }
     }
 
     /**
@@ -371,8 +527,12 @@
      *                                  cause of the failure.
      */
     public void updateUidLockdownRule(final int uid, final boolean add) {
-        final int err = native_updateUidLockdownRule(uid, add);
-        maybeThrow(err, "Unable to update lockdown rule");
+        throwIfPreT("updateUidLockdownRule is not available on pre-T devices");
+        if (add) {
+            addRule(uid, LOCKDOWN_VPN_MATCH, "updateUidLockdownRule");
+        } else {
+            removeRule(uid, LOCKDOWN_VPN_MATCH, "updateUidLockdownRule");
+        }
     }
 
     /**
@@ -397,7 +557,7 @@
      * @throws RemoteException when netd has crashed.
      */
     public void setNetPermForUids(final int permissions, final int[] uids) throws RemoteException {
-        if (USE_NETD) {
+        if (PRE_T) {
             mNetd.trafficSetNetPermForUids(permissions, uids);
             return;
         }
@@ -413,7 +573,7 @@
      */
     public void dump(final FileDescriptor fd, boolean verbose)
             throws IOException, ServiceSpecificException {
-        if (USE_NETD) {
+        if (PRE_T) {
             throw new ServiceSpecificException(
                     EOPNOTSUPP, "dumpsys connectivity trafficcontroller dump not available on pre-T"
                     + " devices, use dumpsys netd trafficcontroller instead.");
@@ -422,14 +582,23 @@
     }
 
     private static native void native_init();
+    @GuardedBy("sUidOwnerMap")
     private native int native_addNaughtyApp(int uid);
+    @GuardedBy("sUidOwnerMap")
     private native int native_removeNaughtyApp(int uid);
+    @GuardedBy("sUidOwnerMap")
     private native int native_addNiceApp(int uid);
+    @GuardedBy("sUidOwnerMap")
     private native int native_removeNiceApp(int uid);
+    @GuardedBy("sUidOwnerMap")
     private native int native_replaceUidChain(String name, boolean isAllowlist, int[] uids);
+    @GuardedBy("sUidOwnerMap")
     private native int native_setUidRule(int childChain, int uid, int firewallRule);
+    @GuardedBy("sUidOwnerMap")
     private native int native_addUidInterfaceRules(String ifName, int[] uids);
+    @GuardedBy("sUidOwnerMap")
     private native int native_removeUidInterfaceRules(int[] uids);
+    @GuardedBy("sUidOwnerMap")
     private native int native_updateUidLockdownRule(int uid, boolean add);
     private native int native_swapActiveStatsMap();
     private native void native_setPermissionForUids(int permissions, int[] uids);
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 6568654..7050b42 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -98,6 +98,9 @@
 import static android.system.OsConstants.IPPROTO_UDP;
 
 import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
+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;
 
 import static java.util.Map.Entry;
 
@@ -450,7 +453,7 @@
      * direct device-originated data traffic of the specific UIDs to the correct
      * default network for each app.
      * Order ints passed to netd must be in the 0~999 range. Larger values code for
-     * a lower priority, {@see NativeUidRangeConfig}
+     * a lower priority, see {@link NativeUidRangeConfig}.
      *
      * Requests that don't code for a per-app preference use PREFERENCE_ORDER_INVALID.
      * The default request uses PREFERENCE_ORDER_DEFAULT.
@@ -1956,7 +1959,7 @@
 
     @Override
     public Network getActiveNetworkForUid(int uid, boolean ignoreBlocked) {
-        PermissionUtils.enforceNetworkStackPermission(mContext);
+        enforceNetworkStackPermission(mContext);
         return getActiveNetworkForUidInternal(uid, ignoreBlocked);
     }
 
@@ -1979,7 +1982,7 @@
 
     @Override
     public NetworkInfo getActiveNetworkInfoForUid(int uid, boolean ignoreBlocked) {
-        PermissionUtils.enforceNetworkStackPermission(mContext);
+        enforceNetworkStackPermission(mContext);
         final NetworkAgentInfo nai = getNetworkAgentInfoForUid(uid);
         if (nai == null) return null;
         return getFilteredNetworkInfo(nai, uid, ignoreBlocked);
@@ -2518,7 +2521,7 @@
     @Override
     public NetworkState[] getAllNetworkState() {
         // This contains IMSI details, so make sure the caller is privileged.
-        PermissionUtils.enforceNetworkStackPermission(mContext);
+        enforceNetworkStackPermission(mContext);
 
         final ArrayList<NetworkState> result = new ArrayList<>();
         for (NetworkStateSnapshot snapshot : getAllNetworkStateSnapshots()) {
@@ -2783,15 +2786,6 @@
         setUidBlockedReasons(uid, blockedReasons);
     }
 
-    private boolean checkAnyPermissionOf(String... permissions) {
-        for (String permission : permissions) {
-            if (mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED) {
-                return true;
-            }
-        }
-        return false;
-    }
-
     private boolean checkAnyPermissionOf(int pid, int uid, String... permissions) {
         for (String permission : permissions) {
             if (mContext.checkPermission(permission, pid, uid) == PERMISSION_GRANTED) {
@@ -2801,13 +2795,6 @@
         return false;
     }
 
-    private void enforceAnyPermissionOf(String... permissions) {
-        if (!checkAnyPermissionOf(permissions)) {
-            throw new SecurityException("Requires one of the following permissions: "
-                    + String.join(", ", permissions) + ".");
-        }
-    }
-
     private void enforceInternetPermission() {
         mContext.enforceCallingOrSelfPermission(
                 android.Manifest.permission.INTERNET,
@@ -2867,7 +2854,7 @@
     }
 
     private void enforceSettingsPermission() {
-        enforceAnyPermissionOf(
+        enforceAnyPermissionOf(mContext,
                 android.Manifest.permission.NETWORK_SETTINGS,
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
     }
@@ -2875,7 +2862,7 @@
     private void enforceNetworkFactoryPermission() {
         // TODO: Check for the BLUETOOTH_STACK permission once that is in the API surface.
         if (UserHandle.getAppId(getCallingUid()) == Process.BLUETOOTH_UID) return;
-        enforceAnyPermissionOf(
+        enforceAnyPermissionOf(mContext,
                 android.Manifest.permission.NETWORK_FACTORY,
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
     }
@@ -2883,7 +2870,7 @@
     private void enforceNetworkFactoryOrSettingsPermission() {
         // TODO: Check for the BLUETOOTH_STACK permission once that is in the API surface.
         if (UserHandle.getAppId(getCallingUid()) == Process.BLUETOOTH_UID) return;
-        enforceAnyPermissionOf(
+        enforceAnyPermissionOf(mContext,
                 android.Manifest.permission.NETWORK_SETTINGS,
                 android.Manifest.permission.NETWORK_FACTORY,
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
@@ -2892,7 +2879,7 @@
     private void enforceNetworkFactoryOrTestNetworksPermission() {
         // TODO: Check for the BLUETOOTH_STACK permission once that is in the API surface.
         if (UserHandle.getAppId(getCallingUid()) == Process.BLUETOOTH_UID) return;
-        enforceAnyPermissionOf(
+        enforceAnyPermissionOf(mContext,
                 android.Manifest.permission.MANAGE_TEST_NETWORKS,
                 android.Manifest.permission.NETWORK_FACTORY,
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
@@ -2909,7 +2896,7 @@
     }
 
     private boolean checkSettingsPermission() {
-        return checkAnyPermissionOf(
+        return PermissionUtils.checkAnyPermissionOf(mContext,
                 android.Manifest.permission.NETWORK_SETTINGS,
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
     }
@@ -2922,27 +2909,21 @@
     }
 
     private void enforceNetworkStackOrSettingsPermission() {
-        enforceAnyPermissionOf(
-                android.Manifest.permission.NETWORK_SETTINGS,
-                android.Manifest.permission.NETWORK_STACK,
-                NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+        enforceNetworkStackPermissionOr(mContext,
+                android.Manifest.permission.NETWORK_SETTINGS);
     }
 
     private void enforceNetworkStackSettingsOrSetup() {
-        enforceAnyPermissionOf(
+        enforceNetworkStackPermissionOr(mContext,
                 android.Manifest.permission.NETWORK_SETTINGS,
-                android.Manifest.permission.NETWORK_SETUP_WIZARD,
-                android.Manifest.permission.NETWORK_STACK,
-                NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+                android.Manifest.permission.NETWORK_SETUP_WIZARD);
     }
 
     private void enforceAirplaneModePermission() {
-        enforceAnyPermissionOf(
+        enforceNetworkStackPermissionOr(mContext,
                 android.Manifest.permission.NETWORK_AIRPLANE_MODE,
                 android.Manifest.permission.NETWORK_SETTINGS,
-                android.Manifest.permission.NETWORK_SETUP_WIZARD,
-                android.Manifest.permission.NETWORK_STACK,
-                NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+                android.Manifest.permission.NETWORK_SETUP_WIZARD);
     }
 
     private void enforceOemNetworkPreferencesPermission() {
@@ -2958,7 +2939,7 @@
     }
 
     private boolean checkNetworkStackPermission() {
-        return checkAnyPermissionOf(
+        return PermissionUtils.checkAnyPermissionOf(mContext,
                 android.Manifest.permission.NETWORK_STACK,
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
     }
@@ -3422,6 +3403,17 @@
         pw.increaseIndent();
         mNetworkActivityTracker.dump(pw);
         pw.decreaseIndent();
+
+        // pre-T is logged by netd.
+        if (SdkLevel.isAtLeastT()) {
+            pw.println();
+            pw.println("BPF programs & maps:");
+            pw.increaseIndent();
+            // Flush is required. Otherwise, the traces in fd can interleave with traces in pw.
+            pw.flush();
+            dumpTrafficController(pw, fd, /*verbose=*/ true);
+            pw.decreaseIndent();
+        }
     }
 
     private void dumpNetworks(IndentingPrintWriter pw) {
@@ -5735,7 +5727,7 @@
 
     @Override
     public void setGlobalProxy(@Nullable final ProxyInfo proxyProperties) {
-        PermissionUtils.enforceNetworkStackPermission(mContext);
+        enforceNetworkStackPermission(mContext);
         mProxyTracker.setGlobalProxy(proxyProperties);
     }
 
@@ -6352,7 +6344,7 @@
             if (null != satisfier) {
                 // If the old NRI was satisfied by an NAI, then it may have had an active request.
                 // The active request is necessary to figure out what callbacks to send, in
-                // particular then a network updates its capabilities.
+                // particular when a network updates its capabilities.
                 // As this code creates a new NRI with a new set of requests, figure out which of
                 // the list of requests should be the active request. It is always the first
                 // request of the list that can be satisfied by the satisfier since the order of
@@ -7283,7 +7275,7 @@
         Objects.requireNonNull(initialScore, "initialScore must not be null");
         Objects.requireNonNull(networkAgentConfig, "networkAgentConfig must not be null");
         if (networkCapabilities.hasTransport(TRANSPORT_TEST)) {
-            enforceAnyPermissionOf(Manifest.permission.MANAGE_TEST_NETWORKS);
+            enforceAnyPermissionOf(mContext, Manifest.permission.MANAGE_TEST_NETWORKS);
         } else {
             enforceNetworkFactoryPermission();
         }
@@ -10296,7 +10288,8 @@
         Objects.requireNonNull(network, "network must not be null");
         Objects.requireNonNull(extras, "extras must not be null");
 
-        enforceAnyPermissionOf(android.Manifest.permission.MANAGE_TEST_NETWORKS,
+        enforceAnyPermissionOf(mContext,
+                android.Manifest.permission.MANAGE_TEST_NETWORKS,
                 android.Manifest.permission.NETWORK_STACK);
         final NetworkCapabilities nc = getNetworkCapabilitiesInternal(network);
         if (!nc.hasTransport(TRANSPORT_TEST)) {
@@ -10704,7 +10697,7 @@
             preferences.add(pref);
         }
 
-        PermissionUtils.enforceNetworkStackPermission(mContext);
+        enforceNetworkStackPermission(mContext);
         if (DBG) {
             log("setProfileNetworkPreferences " + profile + " to " + preferences);
         }
@@ -11387,7 +11380,7 @@
     public boolean getFirewallChainEnabled(final int chain) {
         enforceNetworkStackOrSettingsPermission();
 
-        return mBpfNetMaps.getChainEnabled(chain);
+        return mBpfNetMaps.isChainEnabled(chain);
     }
 
     @Override
diff --git a/service/src/com/android/server/TestNetworkService.java b/service/src/com/android/server/TestNetworkService.java
index 1209579..15d9f13 100644
--- a/service/src/com/android/server/TestNetworkService.java
+++ b/service/src/com/android/server/TestNetworkService.java
@@ -47,7 +47,6 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.net.module.util.NetdUtils;
 import com.android.net.module.util.NetworkStackConstants;
 
 import java.io.IOException;
@@ -83,6 +82,8 @@
     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");
@@ -120,7 +121,7 @@
      */
     @Override
     public TestNetworkInterface createInterface(boolean isTun, boolean hasCarrier, boolean bringUp,
-            LinkAddress[] linkAddrs, @Nullable String iface) {
+            boolean disableIpv6ProvisioningDelay, LinkAddress[] linkAddrs, @Nullable String iface) {
         enforceTestNetworkPermissions(mContext);
 
         Objects.requireNonNull(linkAddrs, "missing linkAddrs");
@@ -137,6 +138,14 @@
         try {
             ParcelFileDescriptor tunIntf = ParcelFileDescriptor.adoptFd(
                     nativeCreateTunTap(isTun, hasCarrier, interfaceName));
+
+            // Disable DAD and remove router_solicitation_delay before assigning link addresses.
+            if (disableIpv6ProvisioningDelay) {
+                mNetd.setProcSysNet(
+                        INetd.IPV6, INetd.CONF, interfaceName, "router_solicitation_delay", "0");
+                mNetd.setProcSysNet(INetd.IPV6, INetd.CONF, interfaceName, "dad_transmits", "0");
+            }
+
             for (LinkAddress addr : linkAddrs) {
                 mNetd.interfaceAddAddress(
                         interfaceName,
@@ -145,7 +154,7 @@
             }
 
             if (bringUp) {
-                NetdUtils.setInterfaceUp(mNetd, interfaceName);
+                nativeBringUpInterface(interfaceName);
             }
 
             return new TestNetworkInterface(tunIntf, interfaceName);
diff --git a/service/src/com/android/server/UidOwnerValue.java b/service/src/com/android/server/UidOwnerValue.java
new file mode 100644
index 0000000..f89e354
--- /dev/null
+++ b/service/src/com/android/server/UidOwnerValue.java
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+import com.android.net.module.util.Struct;
+
+/** Value type for per uid traffic control configuration map  */
+public class UidOwnerValue extends Struct {
+    // Allowed interface index. Only applicable if IIF_MATCH is set in the rule bitmask below.
+    @Field(order = 0, type = Type.U32)
+    public final long iif;
+
+    // A bitmask of match type.
+    @Field(order = 1, type = Type.U32)
+    public final long rule;
+
+    public UidOwnerValue(final long iif, final long rule) {
+        this.iif = iif;
+        this.rule = rule;
+    }
+}
diff --git a/service/src/com/android/server/connectivity/ClatCoordinator.java b/service/src/com/android/server/connectivity/ClatCoordinator.java
index 498cf63..5ea586a 100644
--- a/service/src/com/android/server/connectivity/ClatCoordinator.java
+++ b/service/src/com/android/server/connectivity/ClatCoordinator.java
@@ -347,6 +347,19 @@
                     && this.pid == that.pid
                     && this.cookie == that.cookie;
         }
+
+        @Override
+        public String toString() {
+            return "iface: " + iface
+                    + " (" + ifIndex + ")"
+                    + ", v4iface: " + v4iface
+                    + " (" + v4ifIndex + ")"
+                    + ", v4: " + v4
+                    + ", v6: " + v6
+                    + ", pfx96: " + pfx96
+                    + ", pid: " + pid
+                    + ", cookie: " + cookie;
+        }
     };
 
     @VisibleForTesting
@@ -819,9 +832,9 @@
      * @param pw print writer.
      */
     public void dump(@NonNull IndentingPrintWriter pw) {
-        // TODO: dump ClatdTracker
         // TODO: move map dump to a global place to avoid duplicate dump while there are two or
         // more IPv6 only networks.
+        pw.println("CLAT tracker: " + mClatdTracker.toString());
         pw.println("Forwarding rules:");
         pw.increaseIndent();
         dumpBpfIngress(pw);
diff --git a/service/src/com/android/server/connectivity/DscpPolicyTracker.java b/service/src/com/android/server/connectivity/DscpPolicyTracker.java
index 7829d1a..0e9b459 100644
--- a/service/src/com/android/server/connectivity/DscpPolicyTracker.java
+++ b/service/src/com/android/server/connectivity/DscpPolicyTracker.java
@@ -52,12 +52,12 @@
 
     private static final String TAG = DscpPolicyTracker.class.getSimpleName();
     private static final String PROG_PATH =
-            "/sys/fs/bpf/net_shared/prog_dscp_policy_schedcls_set_dscp";
+            "/sys/fs/bpf/net_shared/prog_dscpPolicy_schedcls_set_dscp";
     // Name is "map + *.o + map_name + map". Can probably shorten this
     private static final String IPV4_POLICY_MAP_PATH = makeMapPath(
-            "dscp_policy_ipv4_dscp_policies");
+            "dscpPolicy_ipv4_dscp_policies");
     private static final String IPV6_POLICY_MAP_PATH = makeMapPath(
-            "dscp_policy_ipv6_dscp_policies");
+            "dscpPolicy_ipv6_dscp_policies");
     private static final int MAX_POLICIES = 16;
 
     private static String makeMapPath(String which) {
@@ -213,7 +213,7 @@
     }
 
     /**
-     * Add the provided DSCP policy to the bpf map. Attach bpf program dscp_policy to iface
+     * Add the provided DSCP policy to the bpf map. Attach bpf program dscpPolicy to iface
      * if not already attached. Response will be sent back to nai with status.
      *
      * DSCP_POLICY_STATUS_SUCCESS - if policy was added successfully
diff --git a/service/src/com/android/server/connectivity/FullScore.java b/service/src/com/android/server/connectivity/FullScore.java
index b13ba93..b156045 100644
--- a/service/src/com/android/server/connectivity/FullScore.java
+++ b/service/src/com/android/server/connectivity/FullScore.java
@@ -76,16 +76,16 @@
     public static final int POLICY_IS_VPN = 62;
 
     // This network has been selected by the user manually from settings or a 3rd party app
-    // at least once. {@see NetworkAgentConfig#explicitlySelected}.
+    // at least once. @see NetworkAgentConfig#explicitlySelected.
     /** @hide */
     public static final int POLICY_EVER_USER_SELECTED = 61;
 
     // The user has indicated in UI that this network should be used even if it doesn't
-    // validate. {@see NetworkAgentConfig#acceptUnvalidated}.
+    // validate. @see NetworkAgentConfig#acceptUnvalidated.
     /** @hide */
     public static final int POLICY_ACCEPT_UNVALIDATED = 60;
 
-    // This network is unmetered. {@see NetworkCapabilities.NET_CAPABILITY_NOT_METERED}.
+    // This network is unmetered. @see NetworkCapabilities.NET_CAPABILITY_NOT_METERED.
     /** @hide */
     public static final int POLICY_IS_UNMETERED = 59;
 
diff --git a/service/src/com/android/server/connectivity/PermissionMonitor.java b/service/src/com/android/server/connectivity/PermissionMonitor.java
index 34c6d2d..fd1ed60 100755
--- a/service/src/com/android/server/connectivity/PermissionMonitor.java
+++ b/service/src/com/android/server/connectivity/PermissionMonitor.java
@@ -51,7 +51,6 @@
 import android.net.INetd;
 import android.net.UidRange;
 import android.net.Uri;
-import android.net.util.SharedLog;
 import android.os.Build;
 import android.os.Process;
 import android.os.RemoteException;
@@ -70,6 +69,7 @@
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.SharedLog;
 import com.android.networkstack.apishim.ProcessShimImpl;
 import com.android.networkstack.apishim.common.ProcessShim;
 import com.android.server.BpfNetMaps;
diff --git a/tests/common/Android.bp b/tests/common/Android.bp
index 58731e0..5c9cc63 100644
--- a/tests/common/Android.bp
+++ b/tests/common/Android.bp
@@ -21,9 +21,22 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+// The target SDK version of the "latest released SDK" CTS tests.
+// This should be updated soon after a new SDK level is finalized.
+// It is different from the target SDK version of production code (e.g., the Tethering,
+// NetworkStack, and CaptivePortalLogin APKs):
+// - The target SDK of production code influences the behaviour of the production code.
+// - The target SDK of the CTS tests validates the behaviour seen by apps that call production APIs.
+// - The behaviour seen by apps that target previous SDKs is tested by previous CTS versions
+//   (currently, CTS 10, 11, and 12).
+java_defaults {
+    name: "ConnectivityTestsLatestSdkDefaults",
+    target_sdk_version: "33",
+}
+
 java_library {
     name: "FrameworksNetCommonTests",
-    defaults: ["framework-connectivity-test-defaults"],
+    defaults: ["framework-connectivity-internal-test-defaults"],
     srcs: [
         "java/**/*.java",
         "java/**/*.kt",
@@ -49,6 +62,7 @@
 // jarjar stops at the first matching rule, so order of concatenation affects the output.
 genrule {
     name: "ConnectivityCoverageJarJarRules",
+    defaults: ["jarjar-rules-combine-defaults"],
     srcs: [
         "tethering-jni-jarjar-rules.txt",
         ":connectivity-jarjar-rules",
@@ -56,8 +70,6 @@
         ":NetworkStackJarJarRules",
     ],
     out: ["jarjar-rules-connectivity-coverage.txt"],
-    // Concat files with a line break in the middle
-    cmd: "for src in $(in); do cat $${src}; echo; done > $(out)",
     visibility: ["//visibility:private"],
 }
 
@@ -81,10 +93,10 @@
     name: "ConnectivityCoverageTests",
     // Tethering started on SDK 30
     min_sdk_version: "30",
-    target_sdk_version: "31",
     test_suites: ["general-tests", "mts-tethering"],
     defaults: [
-        "framework-connectivity-test-defaults",
+        "ConnectivityTestsLatestSdkDefaults",
+        "framework-connectivity-internal-test-defaults",
         "FrameworksNetTests-jni-defaults",
         "libnetworkstackutilsjni_deps",
     ],
diff --git a/tests/common/java/android/net/LinkPropertiesTest.java b/tests/common/java/android/net/LinkPropertiesTest.java
index 9ed2bb3..9506fc9 100644
--- a/tests/common/java/android/net/LinkPropertiesTest.java
+++ b/tests/common/java/android/net/LinkPropertiesTest.java
@@ -1312,7 +1312,26 @@
         assertEquals(3, lp.getRoutes().size());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R) @IgnoreAfter(Build.VERSION_CODES.S_V2)
+    @CtsNetTestCasesMaxTargetSdk31(reason = "Compat change cannot be overridden when targeting T+")
+    @DisableCompatChanges({LinkProperties.EXCLUDED_ROUTES})
+    public void testExcludedRoutesDisabled_S() {
+        final LinkProperties lp = new LinkProperties();
+        assertEquals(0, lp.getRoutes().size());
+
+        lp.addRoute(new RouteInfo(new IpPrefix(ADDRV4, 0), RTN_UNREACHABLE));
+        assertEquals(1, lp.getRoutes().size());
+
+        lp.addRoute(new RouteInfo(new IpPrefix(ADDRV6, 5), RTN_THROW));
+        // RTN_THROW routes are visible on S when added by the caller (but they are not added by
+        // the system). This is uncommon usage but was tested by CTSv12.
+        assertEquals(2, lp.getRoutes().size());
+
+        lp.addRoute(new RouteInfo(new IpPrefix(ADDRV6, 2), RTN_UNICAST));
+        assertEquals(3, lp.getRoutes().size());
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
     @CtsNetTestCasesMaxTargetSdk31(reason = "Compat change cannot be overridden when targeting T+")
     @DisableCompatChanges({LinkProperties.EXCLUDED_ROUTES})
     public void testExcludedRoutesDisabled() {
diff --git a/tests/common/java/android/net/NetworkProviderTest.kt b/tests/common/java/android/net/NetworkProviderTest.kt
index 3ceacf8..c0e7f61 100644
--- a/tests/common/java/android/net/NetworkProviderTest.kt
+++ b/tests/common/java/android/net/NetworkProviderTest.kt
@@ -30,6 +30,7 @@
 import android.os.Looper
 import android.util.Log
 import androidx.test.InstrumentationRegistry
+import com.android.modules.utils.build.SdkLevel.isAtLeastS
 import com.android.net.module.util.ArrayTrackRecord
 import com.android.testutils.CompatUtil
 import com.android.testutils.ConnectivityModuleTest
@@ -38,7 +39,6 @@
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
 import com.android.testutils.TestableNetworkOfferCallback
-import com.android.testutils.isDevSdkInRange
 import org.junit.After
 import org.junit.Before
 import org.junit.Rule
@@ -376,7 +376,7 @@
         doReturn(mCm).`when`(mockContext).getSystemService(Context.CONNECTIVITY_SERVICE)
         val provider = createNetworkProvider(mockContext)
         // ConnectivityManager not required at creation time after R
-        if (!isDevSdkInRange(0, Build.VERSION_CODES.R)) {
+        if (isAtLeastS()) {
             verifyNoMoreInteractions(mockContext)
         }
 
diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/IRemoteSocketFactory.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IRemoteSocketFactory.aidl
index 68176ad..6986e7e 100644
--- a/tests/cts/hostside/aidl/com/android/cts/net/hostside/IRemoteSocketFactory.aidl
+++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IRemoteSocketFactory.aidl
@@ -20,6 +20,7 @@
 
 interface IRemoteSocketFactory {
     ParcelFileDescriptor openSocketFd(String host, int port, int timeoutMs);
+    ParcelFileDescriptor openDatagramSocketFd();
     String getPackageName();
     int getUid();
 }
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/RemoteSocketFactoryClient.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RemoteSocketFactoryClient.java
index 80f99b6..01fbd66 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/RemoteSocketFactoryClient.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RemoteSocketFactoryClient.java
@@ -83,9 +83,19 @@
     public FileDescriptor openSocketFd(String host, int port, int timeoutMs)
             throws RemoteException, ErrnoException, IOException {
         // Dup the filedescriptor so ParcelFileDescriptor's finalizer doesn't garbage collect it
-        // and cause our fd to become invalid. http://b/35927643 .
-        ParcelFileDescriptor pfd = mService.openSocketFd(host, port, timeoutMs);
-        FileDescriptor fd = Os.dup(pfd.getFileDescriptor());
+        // and cause fd to become invalid. http://b/35927643.
+        final ParcelFileDescriptor pfd = mService.openSocketFd(host, port, timeoutMs);
+        final FileDescriptor fd = Os.dup(pfd.getFileDescriptor());
+        pfd.close();
+        return fd;
+    }
+
+    public FileDescriptor openDatagramSocketFd()
+            throws RemoteException, ErrnoException, IOException {
+        // Dup the filedescriptor so ParcelFileDescriptor's finalizer doesn't garbage collect it
+        // and cause fd to become invalid. http://b/35927643.
+        final ParcelFileDescriptor pfd = mService.openDatagramSocketFd();
+        final FileDescriptor fd = Os.dup(pfd.getFileDescriptor());
         pfd.close();
         return fd;
     }
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
index dd8b523..5f032be 100755
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
@@ -16,6 +16,7 @@
 
 package com.android.cts.net.hostside;
 
+import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
 import static android.Manifest.permission.NETWORK_SETTINGS;
 import static android.content.pm.PackageManager.FEATURE_TELEPHONY;
 import static android.content.pm.PackageManager.FEATURE_WIFI;
@@ -35,6 +36,8 @@
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+import static com.android.networkstack.apishim.ConstantsShim.BLOCKED_REASON_LOCKDOWN_VPN;
+import static com.android.networkstack.apishim.ConstantsShim.BLOCKED_REASON_NONE;
 import static com.android.testutils.Cleanup.testAndCleanup;
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
@@ -59,12 +62,15 @@
 import android.database.Cursor;
 import android.net.ConnectivityManager;
 import android.net.ConnectivityManager.NetworkCallback;
+import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
 import android.net.Proxy;
 import android.net.ProxyInfo;
+import android.net.TestNetworkInterface;
+import android.net.TestNetworkManager;
 import android.net.TransportInfo;
 import android.net.Uri;
 import android.net.VpnManager;
@@ -72,6 +78,7 @@
 import android.net.VpnTransportInfo;
 import android.net.cts.util.CtsNetUtils;
 import android.net.wifi.WifiManager;
+import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.ParcelFileDescriptor;
@@ -90,11 +97,13 @@
 import android.test.MoreAsserts;
 import android.text.TextUtils;
 import android.util.Log;
+import android.util.Range;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import com.android.compatibility.common.util.BlockingBroadcastReceiver;
 import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.PacketBuilder;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.RecorderCallback;
@@ -113,12 +122,14 @@
 import java.io.OutputStream;
 import java.net.DatagramPacket;
 import java.net.DatagramSocket;
+import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.ServerSocket;
 import java.net.Socket;
 import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.List;
@@ -164,6 +175,12 @@
     private static final String PRIVATE_DNS_SPECIFIER_SETTING = "private_dns_specifier";
     private static final int NETWORK_CALLBACK_TIMEOUT_MS = 30_000;
 
+    private static final LinkAddress TEST_IP4_DST_ADDR = new LinkAddress("198.51.100.1/24");
+    private static final LinkAddress TEST_IP4_SRC_ADDR = new LinkAddress("198.51.100.2/24");
+    private static final LinkAddress TEST_IP6_DST_ADDR = new LinkAddress("2001:db8:1:3::1/64");
+    private static final LinkAddress TEST_IP6_SRC_ADDR = new LinkAddress("2001:db8:1:3::2/64");
+    private static final short TEST_SRC_PORT = 5555;
+
     public static String TAG = "VpnTest";
     public static int TIMEOUT_MS = 3 * 1000;
     public static int SOCKET_TIMEOUT_MS = 100;
@@ -1572,4 +1589,180 @@
             return future.get(timeout, unit);
         }
     }
+
+    private static final boolean EXPECT_PASS = false;
+    private static final boolean EXPECT_BLOCK = true;
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testBlockIncomingPackets() throws Exception {
+        assumeTrue(supportedHardware());
+        final Network network = mCM.getActiveNetwork();
+        assertNotNull("Requires a working Internet connection", network);
+
+        final int remoteUid = mRemoteSocketFactoryClient.getUid();
+        final List<Range<Integer>> lockdownRange = List.of(new Range<>(remoteUid, remoteUid));
+        final DetailedBlockedStatusCallback remoteUidCallback = new DetailedBlockedStatusCallback();
+
+        // Create a TUN interface
+        final FileDescriptor tunFd = runWithShellPermissionIdentity(() -> {
+            final TestNetworkManager tnm = getInstrumentation().getContext().getSystemService(
+                    TestNetworkManager.class);
+            final TestNetworkInterface iface = tnm.createTunInterface(List.of(
+                    TEST_IP4_DST_ADDR, TEST_IP6_DST_ADDR));
+            return iface.getFileDescriptor().getFileDescriptor();
+        }, MANAGE_TEST_NETWORKS);
+
+        // Create a remote UDP socket
+        final FileDescriptor remoteUdpFd = mRemoteSocketFactoryClient.openDatagramSocketFd();
+
+        testAndCleanup(() -> {
+            runWithShellPermissionIdentity(() -> {
+                mCM.registerDefaultNetworkCallbackForUid(remoteUid, remoteUidCallback,
+                        new Handler(Looper.getMainLooper()));
+            }, NETWORK_SETTINGS);
+            remoteUidCallback.expectAvailableCallbacks(network);
+
+            // The remote UDP socket can receive packets coming from the TUN interface
+            checkBlockIncomingPacket(tunFd, remoteUdpFd, EXPECT_PASS);
+
+            // Lockdown uid that has the remote UDP socket
+            runWithShellPermissionIdentity(() -> {
+                mCM.setRequireVpnForUids(true /* requireVpn */, lockdownRange);
+            }, NETWORK_SETTINGS);
+
+            // setRequireVpnForUids setup a lockdown rule asynchronously. So it needs to wait for
+            // BlockedStatusCallback to be fired before checking the blocking status of incoming
+            // packets.
+            remoteUidCallback.expectBlockedStatusCallback(network, BLOCKED_REASON_LOCKDOWN_VPN);
+
+            if (SdkLevel.isAtLeastT()) {
+                // On T and above, lockdown rule drop packets not coming from lo regardless of the
+                // VPN connectivity.
+                checkBlockIncomingPacket(tunFd, remoteUdpFd, EXPECT_BLOCK);
+            }
+
+            // Start the VPN that has default routes. This VPN should have interface filtering rule
+            // for incoming packet and drop packets not coming from lo nor the VPN interface.
+            final String allowedApps =
+                    mRemoteSocketFactoryClient.getPackageName() + "," + mPackageName;
+            startVpn(new String[]{"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
+                    new String[]{"0.0.0.0/0", "::/0"}, allowedApps, "" /* disallowedApplications */,
+                    null /* proxyInfo */, null /* underlyingNetworks */,
+                    false /* isAlwaysMetered */);
+
+            checkBlockIncomingPacket(tunFd, remoteUdpFd, EXPECT_BLOCK);
+        }, /* cleanup */ () -> {
+                mCM.unregisterNetworkCallback(remoteUidCallback);
+            }, /* cleanup */ () -> {
+                Os.close(tunFd);
+            }, /* cleanup */ () -> {
+                Os.close(remoteUdpFd);
+            }, /* cleanup */ () -> {
+                runWithShellPermissionIdentity(() -> {
+                    mCM.setRequireVpnForUids(false /* requireVpn */, lockdownRange);
+                }, NETWORK_SETTINGS);
+            });
+    }
+
+    private ByteBuffer buildIpv4UdpPacket(final Inet4Address dstAddr, final Inet4Address srcAddr,
+            final short dstPort, final short srcPort, final byte[] payload) throws IOException {
+
+        final ByteBuffer buffer = PacketBuilder.allocate(false /* hasEther */,
+                OsConstants.IPPROTO_IP, OsConstants.IPPROTO_UDP, payload.length);
+        final PacketBuilder packetBuilder = new PacketBuilder(buffer);
+
+        packetBuilder.writeIpv4Header(
+                (byte) 0 /* TOS */,
+                (short) 27149 /* ID */,
+                (short) 0x4000 /* flags=DF, offset=0 */,
+                (byte) 64 /* TTL */,
+                (byte) OsConstants.IPPROTO_UDP,
+                srcAddr,
+                dstAddr);
+        packetBuilder.writeUdpHeader(srcPort, dstPort);
+        buffer.put(payload);
+
+        return packetBuilder.finalizePacket();
+    }
+
+    private ByteBuffer buildIpv6UdpPacket(final Inet6Address dstAddr, final Inet6Address srcAddr,
+            final short dstPort, final short srcPort, final byte[] payload) throws IOException {
+
+        final ByteBuffer buffer = PacketBuilder.allocate(false /* hasEther */,
+                OsConstants.IPPROTO_IPV6, OsConstants.IPPROTO_UDP, payload.length);
+        final PacketBuilder packetBuilder = new PacketBuilder(buffer);
+
+        packetBuilder.writeIpv6Header(
+                0x60000000 /* version=6, traffic class=0, flow label=0 */,
+                (byte) OsConstants.IPPROTO_UDP,
+                (short) 64 /* hop limit */,
+                srcAddr,
+                dstAddr);
+        packetBuilder.writeUdpHeader(srcPort, dstPort);
+        buffer.put(payload);
+
+        return packetBuilder.finalizePacket();
+    }
+
+    private void checkBlockUdp(
+            final FileDescriptor srcTunFd,
+            final FileDescriptor dstUdpFd,
+            final boolean ipv6,
+            final boolean expectBlock) throws Exception {
+        final Random random = new Random();
+        final byte[] sendData = new byte[100];
+        random.nextBytes(sendData);
+        final short dstPort = (short) ((InetSocketAddress) Os.getsockname(dstUdpFd)).getPort();
+
+        ByteBuffer buf;
+        if (ipv6) {
+            buf = buildIpv6UdpPacket(
+                    (Inet6Address) TEST_IP6_DST_ADDR.getAddress(),
+                    (Inet6Address) TEST_IP6_SRC_ADDR.getAddress(),
+                    dstPort, TEST_SRC_PORT, sendData);
+        } else {
+            buf = buildIpv4UdpPacket(
+                    (Inet4Address) TEST_IP4_DST_ADDR.getAddress(),
+                    (Inet4Address) TEST_IP4_SRC_ADDR.getAddress(),
+                    dstPort, TEST_SRC_PORT, sendData);
+        }
+
+        Os.write(srcTunFd, buf);
+
+        final StructPollfd pollfd = new StructPollfd();
+        pollfd.events = (short) POLLIN;
+        pollfd.fd = dstUdpFd;
+        final int ret = Os.poll(new StructPollfd[]{pollfd}, SOCKET_TIMEOUT_MS);
+
+        if (expectBlock) {
+            assertEquals("Expect not to receive a packet but received a packet", 0, ret);
+        } else {
+            assertEquals("Expect to receive a packet but did not receive a packet", 1, ret);
+            final byte[] recvData = new byte[sendData.length];
+            final int readSize = Os.read(dstUdpFd, recvData, 0 /* byteOffset */, recvData.length);
+            assertEquals(recvData.length, readSize);
+            MoreAsserts.assertEquals(sendData, recvData);
+        }
+    }
+
+    private void checkBlockIncomingPacket(
+            final FileDescriptor srcTunFd,
+            final FileDescriptor dstUdpFd,
+            final boolean expectBlock) throws Exception {
+        checkBlockUdp(srcTunFd, dstUdpFd, false /* ipv6 */, expectBlock);
+        checkBlockUdp(srcTunFd, dstUdpFd, true /* ipv6 */, expectBlock);
+    }
+
+    private class DetailedBlockedStatusCallback extends TestableNetworkCallback {
+        public void expectAvailableCallbacks(Network network) {
+            super.expectAvailableCallbacks(network, false /* suspended */, true /* validated */,
+                    BLOCKED_REASON_NONE, NETWORK_CALLBACK_TIMEOUT_MS);
+        }
+        public void expectBlockedStatusCallback(Network network, int blockedStatus) {
+            super.expectBlockedStatusCallback(blockedStatus, network, NETWORK_CALLBACK_TIMEOUT_MS);
+        }
+        public void onBlockedStatusChanged(Network network, int blockedReasons) {
+            getHistory().add(new CallbackEntry.BlockedStatusInt(network, blockedReasons));
+        }
+    }
 }
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/RemoteSocketFactoryService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/RemoteSocketFactoryService.java
index b1b7d77..fb6d16f 100644
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/RemoteSocketFactoryService.java
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/RemoteSocketFactoryService.java
@@ -17,16 +17,17 @@
 package com.android.cts.net.hostside.app2;
 
 import android.app.Service;
-import android.content.Context;
 import android.content.Intent;
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
 import android.os.Process;
-import android.util.Log;
 
 import com.android.cts.net.hostside.IRemoteSocketFactory;
 
+import java.io.UncheckedIOException;
+import java.net.DatagramSocket;
 import java.net.Socket;
+import java.net.SocketException;
 
 
 public class RemoteSocketFactoryService extends Service {
@@ -54,6 +55,16 @@
         public int getUid() {
             return Process.myUid();
         }
+
+        @Override
+        public ParcelFileDescriptor openDatagramSocketFd() {
+            try {
+                final DatagramSocket s = new DatagramSocket();
+                return ParcelFileDescriptor.fromDatagramSocket(s);
+            } catch (SocketException e) {
+                throw new UncheckedIOException(e);
+            }
+        }
     };
 
     @Override
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
index 3821f87..4d90a4a 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
@@ -116,4 +116,8 @@
     public void testInterleavedRoutes() throws Exception {
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testInterleavedRoutes");
     }
+
+    public void testBlockIncomingPackets() throws Exception {
+        runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testBlockIncomingPackets");
+    }
 }
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index a6ed762..62f37bb 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -72,9 +72,9 @@
 android_test {
     name: "CtsNetTestCases",
     defaults: ["CtsNetTestCasesDefaults", "ConnectivityNextEnableDefaults"],
-    // TODO: CTS should not depend on the entirety of the networkstack code.
     static_libs: [
-        "NetworkStackApiCurrentLib",
+        "DhcpPacketLib",
+        "NetworkStackApiCurrentShims",
     ],
     test_suites: [
         "cts",
@@ -86,7 +86,8 @@
     name: "CtsNetTestCasesApiStableDefaults",
     // TODO: CTS should not depend on the entirety of the networkstack code.
     static_libs: [
-        "NetworkStackApiStableLib",
+        "DhcpPacketLib",
+        "NetworkStackApiStableShims",
     ],
     jni_uses_sdk_apis: true,
     min_sdk_version: "29",
@@ -98,10 +99,10 @@
 android_test {
     name: "CtsNetTestCasesLatestSdk",
     defaults: [
+        "ConnectivityTestsLatestSdkDefaults",
         "CtsNetTestCasesDefaults",
         "CtsNetTestCasesApiStableDefaults",
     ],
-    target_sdk_version: "33",
     test_suites: [
         "general-tests",
         "mts-dnsresolver",
diff --git a/tests/cts/net/native/src/BpfCompatTest.cpp b/tests/cts/net/native/src/BpfCompatTest.cpp
index e52533b..5c02b0d 100644
--- a/tests/cts/net/native/src/BpfCompatTest.cpp
+++ b/tests/cts/net/native/src/BpfCompatTest.cpp
@@ -31,7 +31,10 @@
   std::ifstream elfFile(elfPath, std::ios::in | std::ios::binary);
   ASSERT_TRUE(elfFile.is_open());
 
-  if (android::modules::sdklevel::IsAtLeastT()) {
+  if (android::modules::sdklevel::IsAtLeastU()) {
+    EXPECT_EQ(120, readSectionUint("size_of_bpf_map_def", elfFile, 0));
+    EXPECT_EQ(92, readSectionUint("size_of_bpf_prog_def", elfFile, 0));
+  } else if (android::modules::sdklevel::IsAtLeastT()) {
     EXPECT_EQ(116, readSectionUint("size_of_bpf_map_def", elfFile, 0));
     EXPECT_EQ(92, readSectionUint("size_of_bpf_prog_def", elfFile, 0));
   } else {
@@ -47,8 +50,13 @@
 }
 
 TEST(BpfTest, bpfStructSizeTest) {
-  doBpfStructSizeTest("/system/etc/bpf/gpu_mem.o");
-  doBpfStructSizeTest("/system/etc/bpf/time_in_state.o");
+  if (android::modules::sdklevel::IsAtLeastU()) {
+      doBpfStructSizeTest("/system/etc/bpf/gpuMem.o");
+      doBpfStructSizeTest("/system/etc/bpf/timeInState.o");
+  } else {
+      doBpfStructSizeTest("/system/etc/bpf/gpu_mem.o");
+      doBpfStructSizeTest("/system/etc/bpf/time_in_state.o");
+  }
 }
 
 int main(int argc, char **argv) {
diff --git a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
index 1b77d5f..aad8804 100644
--- a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
+++ b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
@@ -37,9 +37,8 @@
 import android.net.cts.NetworkValidationTestUtil.setHttpsUrlDeviceConfig
 import android.net.cts.NetworkValidationTestUtil.setUrlExpirationDeviceConfig
 import android.net.cts.util.CtsNetUtils
-import android.net.util.NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL
-import android.net.util.NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL
-import android.os.Build
+import com.android.net.module.util.NetworkStackConstants.TEST_CAPTIVE_PORTAL_HTTPS_URL
+import com.android.net.module.util.NetworkStackConstants.TEST_CAPTIVE_PORTAL_HTTP_URL
 import android.platform.test.annotations.AppModeFull
 import android.provider.DeviceConfig
 import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
@@ -47,11 +46,11 @@
 import android.util.Log
 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
 import androidx.test.runner.AndroidJUnit4
+import com.android.modules.utils.build.SdkLevel.isAtLeastR
 import com.android.testutils.RecorderCallback
 import com.android.testutils.TestHttpServer
 import com.android.testutils.TestHttpServer.Request
 import com.android.testutils.TestableNetworkCallback
-import com.android.testutils.isDevSdkInRange
 import com.android.testutils.runAsShell
 import fi.iki.elonen.NanoHTTPD.Response.Status
 import junit.framework.AssertionFailedError
@@ -196,8 +195,8 @@
             assertNotEquals(network, cm.activeNetwork, wifiDefaultMessage)
 
             val startPortalAppPermission =
-                    if (isDevSdkInRange(0, Build.VERSION_CODES.Q)) CONNECTIVITY_INTERNAL
-                    else NETWORK_SETTINGS
+                    if (isAtLeastR()) NETWORK_SETTINGS
+                    else CONNECTIVITY_INTERNAL
             runAsShell(startPortalAppPermission) { cm.startCaptivePortalApp(network) }
 
             // Expect the portal content to be fetched at some point after detecting the portal.
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
index 68fa38d..7d1e13f 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
@@ -113,7 +113,7 @@
     private static final int UNKNOWN_DETECTION_METHOD = 4;
     private static final int FILTERED_UNKNOWN_DETECTION_METHOD = 0;
     private static final int CARRIER_CONFIG_CHANGED_BROADCAST_TIMEOUT = 5000;
-    private static final int DELAY_FOR_ADMIN_UIDS_MILLIS = 2000;
+    private static final int DELAY_FOR_ADMIN_UIDS_MILLIS = 5000;
 
     private static final Executor INLINE_EXECUTOR = x -> x.run();
 
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index a416c6a0..64238b3 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -37,9 +37,14 @@
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.net.ConnectivityManager.EXTRA_NETWORK;
 import static android.net.ConnectivityManager.EXTRA_NETWORK_REQUEST;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_POWERSAVE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_RESTRICTED;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_STANDBY;
 import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
 import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
 import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE;
@@ -75,8 +80,6 @@
 import static android.net.cts.util.CtsNetUtils.TEST_HOST;
 import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
 import static android.net.cts.util.CtsTetheringUtils.TestTetheringEventCallback;
-import static android.net.util.NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL;
-import static android.net.util.NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL;
 import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
 import static android.os.Process.INVALID_UID;
 import static android.provider.Settings.Global.NETWORK_METERED_MULTIPATH_PREFERENCE;
@@ -88,6 +91,8 @@
 import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
 import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
+import static com.android.net.module.util.NetworkStackConstants.TEST_CAPTIVE_PORTAL_HTTPS_URL;
+import static com.android.net.module.util.NetworkStackConstants.TEST_CAPTIVE_PORTAL_HTTP_URL;
 import static com.android.networkstack.apishim.ConstantsShim.BLOCKED_REASON_LOCKDOWN_VPN;
 import static com.android.networkstack.apishim.ConstantsShim.BLOCKED_REASON_NONE;
 import static com.android.testutils.Cleanup.testAndCleanup;
@@ -186,7 +191,6 @@
 import com.android.testutils.CompatUtil;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
-import com.android.testutils.DevSdkIgnoreRuleKt;
 import com.android.testutils.DeviceInfoUtils;
 import com.android.testutils.DumpTestUtils;
 import com.android.testutils.RecorderCallback.CallbackEntry;
@@ -331,11 +335,10 @@
         mCtsNetUtils = new CtsNetUtils(mContext);
         mTm = mContext.getSystemService(TelephonyManager.class);
 
-        if (DevSdkIgnoreRuleKt.isDevSdkInRange(null /* minExclusive */,
-                Build.VERSION_CODES.R /* maxInclusive */)) {
-            addLegacySupportedNetworkTypes();
-        } else {
+        if (isAtLeastS()) {
             addSupportedNetworkTypes();
+        } else {
+            addLegacySupportedNetworkTypes();
         }
 
         mUiAutomation = mInstrumentation.getUiAutomation();
@@ -2092,25 +2095,15 @@
 
         try {
             // Verify we cannot set Airplane Mode without correct permission:
-            try {
-                setAndVerifyAirplaneMode(true);
-                fail("SecurityException should have been thrown when setAirplaneMode was called"
-                        + "without holding permission NETWORK_AIRPLANE_MODE.");
-            } catch (SecurityException expected) {}
+            assertThrows(SecurityException.class, () -> setAndVerifyAirplaneMode(true));
 
             // disable airplane mode again to reach a known state
             runShellCommand("cmd connectivity airplane-mode disable");
 
-            // adopt shell permission which holds NETWORK_AIRPLANE_MODE
-            mUiAutomation.adoptShellPermissionIdentity();
+            // Verify we can enable Airplane Mode with correct permission.
+            // TODO: test that NETWORK_AIRPLANE_MODE works as well, once the shell has it.
+            runAsShell(NETWORK_SETTINGS, () -> setAndVerifyAirplaneMode(true));
 
-            // Verify we can enable Airplane Mode with correct permission:
-            try {
-                setAndVerifyAirplaneMode(true);
-            } catch (SecurityException e) {
-                fail("SecurityException should not have been thrown when setAirplaneMode(true) was"
-                        + "called whilst holding the NETWORK_AIRPLANE_MODE permission.");
-            }
             // Verify that the enabling airplane mode takes effect as expected to prevent flakiness
             // caused by fast airplane mode switches. Ensure network lost before turning off
             // airplane mode.
@@ -2118,12 +2111,8 @@
             if (supportTelephony) waitForLost(telephonyCb);
 
             // Verify we can disable Airplane Mode with correct permission:
-            try {
-                setAndVerifyAirplaneMode(false);
-            } catch (SecurityException e) {
-                fail("SecurityException should not have been thrown when setAirplaneMode(false) was"
-                        + "called whilst holding the NETWORK_AIRPLANE_MODE permission.");
-            }
+            runAsShell(NETWORK_SETTINGS, () -> setAndVerifyAirplaneMode(false));
+
             // 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();
@@ -2133,7 +2122,6 @@
             // Restore the previous state of airplane mode and permissions:
             runShellCommand("cmd connectivity airplane-mode "
                     + (isAirplaneModeEnabled ? "enable" : "disable"));
-            mUiAutomation.dropShellPermissionIdentity();
         }
     }
 
@@ -3314,84 +3302,76 @@
 
     private static final boolean EXPECT_PASS = false;
     private static final boolean EXPECT_BLOCK = true;
+    private static final boolean ALLOWLIST = true;
+    private static final boolean DENYLIST = false;
 
-    private void doTestFirewallBlockingDenyRule(final int chain) {
+    private void doTestFirewallBlocking(final int chain, final boolean isAllowList) {
+        final int myUid = Process.myUid();
+        final int ruleToAddMatch = isAllowList ? FIREWALL_RULE_ALLOW : FIREWALL_RULE_DENY;
+        final int ruleToRemoveMatch = isAllowList ? FIREWALL_RULE_DENY : FIREWALL_RULE_ALLOW;
+
         runWithShellPermissionIdentity(() -> {
-            try (DatagramSocket srcSock = new DatagramSocket();
-                 DatagramSocket dstSock = new DatagramSocket()) {
+            // Firewall chain status will be restored after the test.
+            final boolean wasChainEnabled = mCm.getFirewallChainEnabled(chain);
+            final DatagramSocket srcSock = new DatagramSocket();
+            final DatagramSocket dstSock = new DatagramSocket();
+            testAndCleanup(() -> {
+                if (wasChainEnabled) {
+                    mCm.setFirewallChainEnabled(chain, false /* enable */);
+                }
                 dstSock.setSoTimeout(SOCKET_TIMEOUT_MS);
 
-                // No global config, No uid config
+                // Chain disabled, UID not on chain.
                 checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
 
-                // Has global config, No uid config
+                // Chain enabled, UID not on chain.
                 mCm.setFirewallChainEnabled(chain, true /* enable */);
-                checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
+                assertTrue(mCm.getFirewallChainEnabled(chain));
+                checkFirewallBlocking(srcSock, dstSock, isAllowList ? EXPECT_BLOCK : EXPECT_PASS);
 
-                // Has global config, Has uid config
-                mCm.setUidFirewallRule(chain, Process.myUid(), FIREWALL_RULE_DENY);
-                checkFirewallBlocking(srcSock, dstSock, EXPECT_BLOCK);
+                // Chain enabled, UID on chain.
+                mCm.setUidFirewallRule(chain, myUid, ruleToAddMatch);
+                checkFirewallBlocking(srcSock, dstSock, isAllowList ?  EXPECT_PASS : EXPECT_BLOCK);
 
-                // No global config, Has uid config
+                // Chain disabled, UID on chain.
                 mCm.setFirewallChainEnabled(chain, false /* enable */);
+                assertFalse(mCm.getFirewallChainEnabled(chain));
                 checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
 
-                // No global config, No uid config
-                mCm.setUidFirewallRule(chain, Process.myUid(), FIREWALL_RULE_ALLOW);
+                // Chain disabled, UID not on chain.
+                mCm.setUidFirewallRule(chain, myUid, ruleToRemoveMatch);
                 checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
-            } finally {
-                mCm.setFirewallChainEnabled(chain, false /* enable */);
-                mCm.setUidFirewallRule(chain, Process.myUid(), FIREWALL_RULE_ALLOW);
-            }
-        }, NETWORK_SETTINGS);
-    }
-
-    private void doTestFirewallBlockingAllowRule(final int chain) {
-        runWithShellPermissionIdentity(() -> {
-            try (DatagramSocket srcSock = new DatagramSocket();
-                 DatagramSocket dstSock = new DatagramSocket()) {
-                dstSock.setSoTimeout(SOCKET_TIMEOUT_MS);
-
-                // No global config, No uid config
-                checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
-
-                // Has global config, No uid config
-                mCm.setFirewallChainEnabled(chain, true /* enable */);
-                checkFirewallBlocking(srcSock, dstSock, EXPECT_BLOCK);
-
-                // Has global config, Has uid config
-                mCm.setUidFirewallRule(chain, Process.myUid(), FIREWALL_RULE_ALLOW);
-                checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
-
-                // No global config, Has uid config
-                mCm.setFirewallChainEnabled(chain, false /* enable */);
-                checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
-
-                // No global config, No uid config
-                mCm.setUidFirewallRule(chain, Process.myUid(), FIREWALL_RULE_DENY);
-                checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
-            } finally {
-                mCm.setFirewallChainEnabled(chain, false /* enable */);
-                mCm.setUidFirewallRule(chain, Process.myUid(), FIREWALL_RULE_DENY);
-            }
+            }, /* cleanup */ () -> {
+                    srcSock.close();
+                    dstSock.close();
+                }, /* cleanup */ () -> {
+                    // Restore the global chain status
+                    mCm.setFirewallChainEnabled(chain, wasChainEnabled);
+                }, /* cleanup */ () -> {
+                    try {
+                        mCm.setUidFirewallRule(chain, myUid, ruleToRemoveMatch);
+                    } catch (IllegalStateException ignored) {
+                        // Removing match causes an exception when the rule entry for the uid does
+                        // not exist. But this is fine and can be ignored.
+                    }
+                });
         }, NETWORK_SETTINGS);
     }
 
     @Test @IgnoreUpTo(SC_V2)
     @AppModeFull(reason = "Socket cannot bind in instant app mode")
     public void testFirewallBlocking() {
-        // Following tests affect the actual state of networking on the device after the test.
-        // This might cause unexpected behaviour of the device. So, we skip them for now.
-        // We will enable following tests after adding the logic of firewall state restoring.
-        // doTestFirewallBlockingAllowRule(FIREWALL_CHAIN_DOZABLE);
-        // doTestFirewallBlockingAllowRule(FIREWALL_CHAIN_POWERSAVE);
-        // doTestFirewallBlockingAllowRule(FIREWALL_CHAIN_RESTRICTED);
-        // doTestFirewallBlockingAllowRule(FIREWALL_CHAIN_LOW_POWER_STANDBY);
+        // ALLOWLIST means the firewall denies all by default, uids must be explicitly allowed
+        doTestFirewallBlocking(FIREWALL_CHAIN_DOZABLE, ALLOWLIST);
+        doTestFirewallBlocking(FIREWALL_CHAIN_POWERSAVE, ALLOWLIST);
+        doTestFirewallBlocking(FIREWALL_CHAIN_RESTRICTED, ALLOWLIST);
+        doTestFirewallBlocking(FIREWALL_CHAIN_LOW_POWER_STANDBY, ALLOWLIST);
 
-        // doTestFirewallBlockingDenyRule(FIREWALL_CHAIN_STANDBY);
-        doTestFirewallBlockingDenyRule(FIREWALL_CHAIN_OEM_DENY_1);
-        doTestFirewallBlockingDenyRule(FIREWALL_CHAIN_OEM_DENY_2);
-        doTestFirewallBlockingDenyRule(FIREWALL_CHAIN_OEM_DENY_3);
+        // DENYLIST means the firewall allows all by default, uids must be explicitly denyed
+        doTestFirewallBlocking(FIREWALL_CHAIN_STANDBY, DENYLIST);
+        doTestFirewallBlocking(FIREWALL_CHAIN_OEM_DENY_1, DENYLIST);
+        doTestFirewallBlocking(FIREWALL_CHAIN_OEM_DENY_2, DENYLIST);
+        doTestFirewallBlocking(FIREWALL_CHAIN_OEM_DENY_3, DENYLIST);
     }
 
     private void assumeTestSApis() {
diff --git a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
index bbac09b..b68d3bf 100644
--- a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
+++ b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
@@ -27,6 +27,8 @@
 import android.net.IpPrefix
 import android.net.LinkAddress
 import android.net.LinkProperties
+import android.net.Network
+import android.net.MacAddress
 import android.net.NetworkAgent
 import android.net.NetworkAgent.DSCP_POLICY_STATUS_DELETED
 import android.net.NetworkAgent.DSCP_POLICY_STATUS_SUCCESS
@@ -45,10 +47,13 @@
 import android.net.TestNetworkManager
 import android.net.RouteInfo
 import android.os.HandlerThread
+import android.os.SystemClock
 import android.platform.test.annotations.AppModeFull
+import android.system.ErrnoException
 import android.system.Os
 import android.system.OsConstants.AF_INET
 import android.system.OsConstants.AF_INET6
+import android.system.OsConstants.ENETUNREACH
 import android.system.OsConstants.IPPROTO_UDP
 import android.system.OsConstants.SOCK_DGRAM
 import android.system.OsConstants.SOCK_NONBLOCK
@@ -56,9 +61,15 @@
 import android.util.Range
 import androidx.test.InstrumentationRegistry
 import androidx.test.runner.AndroidJUnit4
+import com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV4
+import com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6
+import com.android.net.module.util.Struct
+import com.android.net.module.util.structs.EthernetHeader
+import com.android.testutils.ArpResponder
 import com.android.testutils.CompatUtil
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.assertParcelingIsLossless
+import com.android.testutils.RouterAdvertisementResponder
 import com.android.testutils.runAsShell
 import com.android.testutils.SC_V2
 import com.android.testutils.TapPacketReader
@@ -74,7 +85,7 @@
 import org.junit.runner.RunWith
 import java.net.Inet4Address
 import java.net.Inet6Address
-import java.net.InetAddress
+import java.net.InetSocketAddress
 import java.nio.ByteBuffer
 import java.nio.ByteOrder
 import java.util.regex.Pattern
@@ -103,10 +114,12 @@
 
     private val LOCAL_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1")
     private val TEST_TARGET_IPV4_ADDR =
-            InetAddresses.parseNumericAddress("8.8.8.8") as Inet4Address
-    private val LOCAL_IPV6_ADDRESS = InetAddresses.parseNumericAddress("2001:db8::1")
+            InetAddresses.parseNumericAddress("203.0.113.1") as Inet4Address
     private val TEST_TARGET_IPV6_ADDR =
-            InetAddresses.parseNumericAddress("2001:4860:4860::8888") as Inet6Address
+        InetAddresses.parseNumericAddress("2001:4860:4860::8888") as Inet6Address
+    private val TEST_ROUTER_IPV6_ADDR =
+        InetAddresses.parseNumericAddress("fe80::1234") as Inet6Address
+    private val TEST_TARGET_MAC_ADDR = MacAddress.fromString("12:34:56:78:9a:bc")
 
     private val realContext = InstrumentationRegistry.getContext()
     private val cm = realContext.getSystemService(ConnectivityManager::class.java)
@@ -116,9 +129,12 @@
 
     private val handlerThread = HandlerThread(DscpPolicyTest::class.java.simpleName)
 
+    private lateinit var srcAddressV6: Inet6Address
     private lateinit var iface: TestNetworkInterface
     private lateinit var tunNetworkCallback: TestNetworkCallback
     private lateinit var reader: TapPacketReader
+    private lateinit var arpResponder: ArpResponder
+    private lateinit var raResponder: RouterAdvertisementResponder
 
     private fun getKernelVersion(): IntArray {
         // Example:
@@ -129,6 +145,7 @@
         return intArrayOf(Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2)))
     }
 
+    // TODO: replace with DeviceInfoUtils#isKernelVersionAtLeast
     private fun kernelIsAtLeast(major: Int, minor: Int): Boolean {
         val version = getKernelVersion()
         return (version.get(0) > major || (version.get(0) == major && version.get(1) >= minor))
@@ -136,15 +153,16 @@
 
     @Before
     fun setUp() {
-        // For BPF support kernel needs to be at least 5.4.
-        assumeTrue(kernelIsAtLeast(5, 4))
+        // For BPF support kernel needs to be at least 5.15.
+        assumeTrue(kernelIsAtLeast(5, 15))
 
         runAsShell(MANAGE_TEST_NETWORKS) {
             val tnm = realContext.getSystemService(TestNetworkManager::class.java)
 
-            iface = tnm.createTunInterface(arrayOf(
-                    LinkAddress(LOCAL_IPV4_ADDRESS, IP4_PREFIX_LEN),
-                    LinkAddress(LOCAL_IPV6_ADDRESS, IP6_PREFIX_LEN)))
+            // Only statically configure the IPv4 address; for IPv6, use the SLAAC generated
+            // address.
+            iface = tnm.createTapInterface(true /* disableIpv6ProvisioningDelay */,
+                    arrayOf(LinkAddress(LOCAL_IPV4_ADDRESS, IP4_PREFIX_LEN)))
             assertNotNull(iface)
         }
 
@@ -154,21 +172,30 @@
                 iface.fileDescriptor.fileDescriptor,
                 MAX_PACKET_LENGTH)
         reader.startAsyncForTest()
+
+        arpResponder = ArpResponder(reader, mapOf(TEST_TARGET_IPV4_ADDR to TEST_TARGET_MAC_ADDR))
+        arpResponder.start()
+        raResponder = RouterAdvertisementResponder(reader)
+        raResponder.addRouterEntry(TEST_TARGET_MAC_ADDR, TEST_ROUTER_IPV6_ADDR)
+        raResponder.start()
     }
 
     @After
     fun tearDown() {
-        if (!kernelIsAtLeast(5, 4)) {
-            return;
+        if (!kernelIsAtLeast(5, 15)) {
+            return
         }
+        raResponder.stop()
+        arpResponder.stop()
+
         agentsToCleanUp.forEach { it.unregister() }
         callbacksToCleanUp.forEach { cm.unregisterNetworkCallback(it) }
 
         // reader.stop() cleans up tun fd
         reader.handler.post { reader.stop() }
-        if (iface.fileDescriptor.fileDescriptor != null)
-            Os.close(iface.fileDescriptor.fileDescriptor)
+        // quitSafely processes all events in the queue, except delayed messages.
         handlerThread.quitSafely()
+        handlerThread.join()
     }
 
     private fun requestNetwork(request: NetworkRequest, callback: TestableNetworkCallback) {
@@ -189,6 +216,39 @@
                 .build()
     }
 
+    private fun waitForGlobalIpv6Address(network: Network): Inet6Address {
+        // Wait for global IPv6 address to be available
+        val sock = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP)
+        network.bindSocket(sock)
+
+        var inet6Addr: Inet6Address? = null
+        val timeout = SystemClock.elapsedRealtime() + PACKET_TIMEOUT_MS
+        while (timeout > SystemClock.elapsedRealtime()) {
+            try {
+                // Pick any arbitrary port
+                Os.connect(sock, TEST_TARGET_IPV6_ADDR, 12345)
+                val sockAddr = Os.getsockname(sock) as InetSocketAddress
+
+                // TODO: make RouterAdvertisementResponder.SLAAC_PREFIX public and use it here,
+                // or make it configurable and configure it here.
+                if (IpPrefix("2001:db8::/64").contains(sockAddr.address)) {
+                    inet6Addr = sockAddr.address as Inet6Address
+                    break
+                }
+            } catch (e: ErrnoException) {
+                // ignore ENETUNREACH -- there may not be an address available yet.
+                if (e.errno != ENETUNREACH) {
+                    Os.close(sock)
+                    throw e
+                }
+            }
+            SystemClock.sleep(10 /* ms */)
+        }
+        Os.close(sock)
+        assertNotNull(inet6Addr)
+        return inet6Addr!!
+    }
+
     private fun createConnectedNetworkAgent(
         context: Context = realContext,
         specifier: String? = iface.getInterfaceName()
@@ -211,9 +271,8 @@
         }
         val lp = LinkProperties().apply {
             addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, IP4_PREFIX_LEN))
-            addLinkAddress(LinkAddress(LOCAL_IPV6_ADDRESS, IP6_PREFIX_LEN))
             addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
-            addRoute(RouteInfo(InetAddress.getByName("fe80::1234")))
+            addRoute(RouteInfo(IpPrefix("::/0"), TEST_ROUTER_IPV6_ADDR))
             setInterfaceName(specifier)
         }
         val config = NetworkAgentConfig.Builder().build()
@@ -226,7 +285,9 @@
         agent.expectCallback<OnNetworkCreated>()
         agent.expectSignalStrengths(intArrayOf())
         agent.expectValidationBypassedStatus()
+
         val network = agent.network ?: fail("Expected a non-null network")
+        srcAddressV6 = waitForGlobalIpv6Address(network)
         return agent to callback
     }
 
@@ -237,7 +298,7 @@
     fun sendPacket(
         agent: TestableNetworkAgent,
         sendV6: Boolean,
-        dstPort: Int = 0,
+        dstPort: Int = 0
     ) {
         val testString = "test string"
         val testPacket = ByteBuffer.wrap(testString.toByteArray(Charsets.UTF_8))
@@ -249,11 +310,11 @@
 
         val originalPacket = testPacket.readAsArray()
         Os.sendto(socket, originalPacket, 0 /* bytesOffset */, originalPacket.size, 0 /* flags */,
-                if(sendV6) TEST_TARGET_IPV6_ADDR else TEST_TARGET_IPV4_ADDR, dstPort)
+                if (sendV6) TEST_TARGET_IPV6_ADDR else TEST_TARGET_IPV4_ADDR, dstPort)
         Os.close(socket)
     }
 
-    fun parseV4PacketDscp(buffer : ByteBuffer) : Int {
+    fun parseV4PacketDscp(buffer: ByteBuffer): Int {
         val ip_ver = buffer.get()
         val tos = buffer.get()
         val length = buffer.getShort()
@@ -265,7 +326,7 @@
         return tos.toInt().shr(2)
     }
 
-    fun parseV6PacketDscp(buffer : ByteBuffer) : Int {
+    fun parseV6PacketDscp(buffer: ByteBuffer): Int {
         val ip_ver = buffer.get()
         val tc = buffer.get()
         val fl = buffer.getShort()
@@ -279,9 +340,9 @@
     }
 
     fun parsePacketIp(
-        buffer : ByteBuffer,
-        sendV6 : Boolean,
-    ) : Boolean {
+        buffer: ByteBuffer,
+        sendV6: Boolean
+    ): Boolean {
         val ipAddr = if (sendV6) ByteArray(16) else ByteArray(4)
         buffer.get(ipAddr)
         val srcIp = if (sendV6) Inet6Address.getByAddress(ipAddr)
@@ -292,20 +353,20 @@
 
         Log.e(TAG, "IP Src:" + srcIp + " dst: " + dstIp)
 
-        if ((sendV6 && srcIp == LOCAL_IPV6_ADDRESS && dstIp == TEST_TARGET_IPV6_ADDR) ||
+        if ((sendV6 && srcIp == srcAddressV6 && dstIp == TEST_TARGET_IPV6_ADDR) ||
                 (!sendV6 && srcIp == LOCAL_IPV4_ADDRESS && dstIp == TEST_TARGET_IPV4_ADDR)) {
-            Log.e(TAG, "IP return true");
+            Log.e(TAG, "IP return true")
             return true
         }
-        Log.e(TAG, "IP return false");
+        Log.e(TAG, "IP return false")
         return false
     }
 
     fun parsePacketPort(
-        buffer : ByteBuffer,
-        srcPort : Int,
-        dstPort : Int
-    ) : Boolean {
+        buffer: ByteBuffer,
+        srcPort: Int,
+        dstPort: Int
+    ): Boolean {
         if (srcPort == 0 && dstPort == 0) return true
 
         val packetSrcPort = buffer.getShort().toInt()
@@ -315,26 +376,33 @@
 
         if ((srcPort == 0 || (srcPort != 0 && srcPort == packetSrcPort)) &&
                 (dstPort == 0 || (dstPort != 0 && dstPort == packetDstPort))) {
-            Log.e(TAG, "Port return true");
+            Log.e(TAG, "Port return true")
             return true
         }
-        Log.e(TAG, "Port return false");
+        Log.e(TAG, "Port return false")
         return false
     }
 
     fun validatePacket(
-        agent : TestableNetworkAgent,
-        sendV6 : Boolean = false,
-        dscpValue : Int = 0,
-        dstPort : Int = 0,
+        agent: TestableNetworkAgent,
+        sendV6: Boolean = false,
+        dscpValue: Int = 0,
+        dstPort: Int = 0
     ) {
-        var packetFound = false;
+        var packetFound = false
         sendPacket(agent, sendV6, dstPort)
         // TODO: grab source port from socket in sendPacket
 
         Log.e(TAG, "find DSCP value:" + dscpValue)
-        generateSequence { reader.poll(PACKET_TIMEOUT_MS) }.forEach { packet ->
+        val packets = generateSequence { reader.poll(PACKET_TIMEOUT_MS) }
+        for (packet in packets) {
             val buffer = ByteBuffer.wrap(packet, 0, packet.size).order(ByteOrder.BIG_ENDIAN)
+            // TODO: consider using Struct.parse for all packet parsing.
+            val etherHdr = Struct.parse(EthernetHeader::class.java, buffer)
+            val expectedType = if (sendV6) ETHER_TYPE_IPV6 else ETHER_TYPE_IPV4
+            if (etherHdr.etherType != expectedType) {
+                continue
+            }
             val dscp = if (sendV6) parseV6PacketDscp(buffer) else parseV4PacketDscp(buffer)
             Log.e(TAG, "DSCP value:" + dscp)
 
@@ -420,7 +488,7 @@
         val policy2 = DscpPolicy.Builder(1, 4)
                 .setDestinationPortRange(Range(5555, 5555))
                 .setDestinationAddress(TEST_TARGET_IPV6_ADDR)
-                .setSourceAddress(LOCAL_IPV6_ADDRESS)
+                .setSourceAddress(srcAddressV6)
                 .setProtocol(IPPROTO_UDP).build()
         agent.sendAddDscpPolicy(policy2)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
index 458d225..89b107e 100644
--- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
@@ -21,6 +21,8 @@
 import android.content.Context
 import android.net.ConnectivityManager
 import android.net.EthernetManager
+import android.net.EthernetManager.ETHERNET_STATE_DISABLED
+import android.net.EthernetManager.ETHERNET_STATE_ENABLED
 import android.net.EthernetManager.InterfaceStateListener
 import android.net.EthernetManager.ROLE_CLIENT
 import android.net.EthernetManager.ROLE_NONE
@@ -35,41 +37,44 @@
 import android.net.EthernetNetworkUpdateRequest
 import android.net.InetAddresses
 import android.net.IpConfiguration
+import android.net.LinkAddress
 import android.net.MacAddress
 import android.net.Network
 import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED
 import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
 import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
 import android.net.NetworkCapabilities.TRANSPORT_TEST
 import android.net.NetworkRequest
+import android.net.StaticIpConfiguration
 import android.net.TestNetworkInterface
 import android.net.TestNetworkManager
+import android.net.cts.EthernetManagerTest.EthernetStateListener.CallbackEntry.EthernetStateChanged
 import android.net.cts.EthernetManagerTest.EthernetStateListener.CallbackEntry.InterfaceStateChanged
 import android.os.Build
 import android.os.Handler
 import android.os.Looper
 import android.os.OutcomeReceiver
-import android.os.SystemProperties
 import android.platform.test.annotations.AppModeFull
 import android.util.ArraySet
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.net.module.util.ArrayTrackRecord
 import com.android.net.module.util.TrackRecord
-import com.android.testutils.anyNetwork
 import com.android.testutils.ConnectivityModuleTest
-import com.android.testutils.DeviceInfoUtils.isKernelVersionAtLeast
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.DeviceInfoUtils.isKernelVersionAtLeast
 import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
 import com.android.testutils.RecorderCallback.CallbackEntry.Lost
 import com.android.testutils.RouterAdvertisementResponder
 import com.android.testutils.TapPacketReader
 import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.anyNetwork
 import com.android.testutils.runAsShell
 import com.android.testutils.waitForIdle
 import org.junit.After
 import org.junit.Assume.assumeTrue
-import org.junit.Assume.assumeFalse
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -77,6 +82,8 @@
 import java.util.concurrent.CompletableFuture
 import java.util.concurrent.ExecutionException
 import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+import java.util.function.IntConsumer
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
 import kotlin.test.assertFalse
@@ -85,17 +92,25 @@
 import kotlin.test.assertTrue
 import kotlin.test.fail
 
+private const val TAG = "EthernetManagerTest"
 // TODO: try to lower this timeout in the future. Currently, ethernet tests are still flaky because
 // the interface is not ready fast enough (mostly due to the up / up / down / up issue).
 private const val TIMEOUT_MS = 2000L
-private const val NO_CALLBACK_TIMEOUT_MS = 200L
+// Timeout used to confirm no callbacks matching given criteria are received. Must be long enough to
+// process all callbacks including ip provisioning when using the updateConfiguration API.
+private const val NO_CALLBACK_TIMEOUT_MS = 500L
+
 private val DEFAULT_IP_CONFIGURATION = IpConfiguration(IpConfiguration.IpAssignment.DHCP,
-    IpConfiguration.ProxySettings.NONE, null, null)
+        IpConfiguration.ProxySettings.NONE, null, null)
 private val ETH_REQUEST: NetworkRequest = NetworkRequest.Builder()
-    .addTransportType(TRANSPORT_TEST)
-    .addTransportType(TRANSPORT_ETHERNET)
-    .removeCapability(NET_CAPABILITY_TRUSTED)
-    .build()
+        .addTransportType(TRANSPORT_TEST)
+        .addTransportType(TRANSPORT_ETHERNET)
+        .removeCapability(NET_CAPABILITY_TRUSTED)
+        .build()
+private val STATIC_IP_CONFIGURATION = IpConfiguration.Builder()
+        .setStaticIpConfiguration(StaticIpConfiguration.Builder()
+                .setIpAddress(LinkAddress("192.0.2.1/30")).build())
+        .build()
 
 @AppModeFull(reason = "Instant apps can't access EthernetManager")
 // EthernetManager is not updatable before T, so tests do not need to be backwards compatible.
@@ -162,7 +177,7 @@
 
     private open class EthernetStateListener private constructor(
         private val history: ArrayTrackRecord<CallbackEntry>
-    ) : InterfaceStateListener,
+    ) : InterfaceStateListener, IntConsumer,
                 TrackRecord<EthernetStateListener.CallbackEntry> by history {
         constructor() : this(ArrayTrackRecord())
 
@@ -175,6 +190,8 @@
                 val role: Int,
                 val configuration: IpConfiguration?
             ) : CallbackEntry()
+
+            data class EthernetStateChanged(val state: Int) : CallbackEntry()
         }
 
         override fun onInterfaceStateChanged(
@@ -186,6 +203,10 @@
             add(InterfaceStateChanged(iface, state, role, cfg))
         }
 
+        override fun accept(state: Int) {
+            add(EthernetStateChanged(state))
+        }
+
         fun <T : CallbackEntry> expectCallback(expected: T): T {
             val event = pollForNextCallback()
             assertEquals(expected, event)
@@ -196,6 +217,10 @@
             expectCallback(createChangeEvent(iface.name, state, role))
         }
 
+        fun expectCallback(state: Int) {
+            expectCallback(EthernetStateChanged(state))
+        }
+
         fun createChangeEvent(iface: String, state: Int, role: Int) =
                 InterfaceStateChanged(iface, state, role,
                         if (state != STATE_ABSENT) DEFAULT_IP_CONFIGURATION else null)
@@ -206,12 +231,12 @@
 
         fun eventuallyExpect(expected: CallbackEntry) = events.poll(TIMEOUT_MS) { it == expected }
 
-        fun eventuallyExpect(interfaceName: String, state: Int, role: Int) {
-            assertNotNull(eventuallyExpect(createChangeEvent(interfaceName, state, role)))
+        fun eventuallyExpect(iface: EthernetTestInterface, state: Int, role: Int) {
+            assertNotNull(eventuallyExpect(createChangeEvent(iface.name, state, role)))
         }
 
-        fun eventuallyExpect(iface: EthernetTestInterface, state: Int, role: Int) {
-            eventuallyExpect(iface.name, state, role)
+        fun eventuallyExpect(state: Int) {
+            assertNotNull(eventuallyExpect(EthernetStateChanged(state)))
         }
 
         fun assertNoCallback() {
@@ -285,11 +310,17 @@
 
     @After
     fun tearDown() {
-        setIncludeTestInterfaces(false)
+        // Reenable ethernet, so ABSENT callbacks are received.
+        setEthernetEnabled(true)
+
         for (iface in createdIfaces) {
             iface.destroy()
             ifaceListener.eventuallyExpect(iface, STATE_ABSENT, ROLE_NONE)
         }
+
+        // After test interfaces are removed, disable tracking.
+        setIncludeTestInterfaces(false)
+
         for (listener in addedListeners) {
             em.removeInterfaceStateListener(listener)
         }
@@ -396,6 +427,19 @@
         }
     }
 
+    private fun setEthernetEnabled(enabled: Boolean) {
+        runAsShell(NETWORK_SETTINGS) { em.setEthernetEnabled(enabled) }
+
+        val listener = EthernetStateListener()
+        em.addEthernetStateListener(handler::post, listener)
+        try {
+            listener.eventuallyExpect(
+                    if (enabled) ETHERNET_STATE_ENABLED else ETHERNET_STATE_DISABLED)
+        } finally {
+            em.removeEthernetStateListener(listener)
+        }
+    }
+
     // NetworkRequest.Builder does not create a copy of the passed NetworkRequest, so in order to
     // keep ETH_REQUEST as it is, a defensive copy is created here.
     private fun NetworkRequest.createCopyWithEthernetSpecifier(ifaceName: String) =
@@ -404,7 +448,10 @@
 
     // It can take multiple seconds for the network to become available.
     private fun TestableNetworkCallback.expectAvailable() =
-        expectCallback<Available>(anyNetwork(), 5000 /* ms timeout */).network
+            expectCallback<Available>(anyNetwork(), 5000 /* ms timeout */).network
+
+    private fun TestableNetworkCallback.expectLost(n: Network = anyNetwork()) =
+            expectCallback<Lost>(n, 5000 /* ms timeout */)
 
     // b/233534110: eventuallyExpect<Lost>() does not advance ReadHead, use
     // eventuallyExpect(Lost::class) instead.
@@ -412,7 +459,9 @@
         eventuallyExpect(Lost::class, TIMEOUT_MS) { n?.equals(it.network) ?: true }
 
     private fun TestableNetworkCallback.assertNeverLost(n: Network? = null) =
-        assertNoCallbackThat() { it is Lost && (n?.equals(it.network) ?: true) }
+        assertNoCallbackThat(NO_CALLBACK_TIMEOUT_MS) {
+            it is Lost && (n?.equals(it.network) ?: true)
+        }
 
     private fun TestableNetworkCallback.assertNeverAvailable(n: Network? = null) =
         assertNoCallbackThat() { it is Available && (n?.equals(it.network) ?: true) }
@@ -422,6 +471,18 @@
             it.networkSpecifier == EthernetNetworkSpecifier(name)
         }
 
+    private fun TestableNetworkCallback.expectCapabilitiesWithCapability(cap: Int) =
+        expectCapabilitiesThat(anyNetwork(), TIMEOUT_MS) {
+            it.hasCapability(cap)
+        }
+
+    private fun TestableNetworkCallback.expectLinkPropertiesWithLinkAddress(addr: LinkAddress) =
+        expectLinkPropertiesThat(anyNetwork(), TIMEOUT_MS) {
+            // LinkAddress.equals isn't possible as the system changes the LinkAddress.flags value.
+            // any() must be used since the interface may also have a link-local address.
+            it.linkAddresses.any { x -> x.isSameAddressAs(addr) }
+        }
+
     @Test
     fun testCallbacks() {
         // If an interface exists when the callback is registered, it is reported on registration.
@@ -458,32 +519,45 @@
         }
     }
 
-    // TODO: this function is now used in two places (EthernetManagerTest and
-    // EthernetTetheringTest), so it should be moved to testutils.
-    private fun isAdbOverNetwork(): Boolean {
-        // If adb TCP port opened, this test may running by adb over network.
-        return (SystemProperties.getInt("persist.adb.tcp.port", -1) > -1 ||
-                SystemProperties.getInt("service.adb.tcp.port", -1) > -1)
+    private fun assumeNoInterfaceForTetheringAvailable() {
+        // Interfaces that have configured NetworkCapabilities will never be used for tethering,
+        // see aosp/2123900.
+        try {
+            // assumeException does not exist.
+            requestTetheredInterface().expectOnAvailable()
+            // interface used for tethering is available, throw an assumption error.
+            assumeTrue(false)
+        } catch (e: TimeoutException) {
+            // do nothing -- the TimeoutException indicates that no interface is available for
+            // tethering.
+            releaseTetheredInterface()
+        }
     }
 
     @Test
     fun testCallbacks_forServerModeInterfaces() {
-        // do not run this test when adb might be connected over ethernet.
-        assumeFalse(isAdbOverNetwork())
+        // do not run this test if an interface that can be used for tethering already exists.
+        assumeNoInterfaceForTetheringAvailable()
+
+        val iface = createInterface()
+        requestTetheredInterface().expectOnAvailable()
 
         val listener = EthernetStateListener()
         addInterfaceStateListener(listener)
-
-        // it is possible that a physical interface is present, so it is not guaranteed that iface
-        // will be put into server mode. This should not matter for the test though. Calling
-        // createInterface() makes sure we have at least one interface available.
-        val iface = createInterface()
-        val cb = requestTetheredInterface()
-        val ifaceName = cb.expectOnAvailable()
-        listener.eventuallyExpect(ifaceName, STATE_LINK_UP, ROLE_SERVER)
+        // TODO(b/236895792): THIS IS A BUG! Existing server mode interfaces are not reported when
+        // an InterfaceStateListener is registered.
+        // Note: using eventuallyExpect as there may be other interfaces present.
+        // listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_SERVER)
 
         releaseTetheredInterface()
-        listener.eventuallyExpect(ifaceName, STATE_LINK_UP, ROLE_CLIENT)
+        listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_CLIENT)
+
+        requestTetheredInterface().expectOnAvailable()
+        // This should be changed to expectCallback, once b/236895792 is fixed.
+        listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_SERVER)
+
+        releaseTetheredInterface()
+        listener.expectCallback(iface, STATE_LINK_UP, ROLE_CLIENT)
     }
 
     /**
@@ -651,4 +725,102 @@
         iface.setCarrierEnabled(false)
         cb.eventuallyExpectLost()
     }
+
+    @Test
+    fun testRemoveInterface_whileInServerMode() {
+        assumeNoInterfaceForTetheringAvailable()
+
+        val listener = EthernetStateListener()
+        addInterfaceStateListener(listener)
+
+        val iface = createInterface()
+        val ifaceName = requestTetheredInterface().expectOnAvailable()
+
+        assertEquals(iface.name, ifaceName)
+        listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_SERVER)
+
+        removeInterface(iface)
+
+        // Note: removeInterface already verifies that a STATE_ABSENT, ROLE_NONE callback is
+        // received, but it can't hurt to explicitly check for it.
+        listener.expectCallback(iface, STATE_ABSENT, ROLE_NONE)
+        releaseTetheredInterface()
+        listener.assertNoCallback()
+    }
+
+    @Test
+    fun testEnableDisableInterface_withActiveRequest() {
+        val iface = createInterface()
+        val cb = requestNetwork(ETH_REQUEST)
+        cb.expectAvailable()
+        cb.assertNeverLost()
+
+        disableInterface(iface).expectResult(iface.name)
+        cb.eventuallyExpectLost()
+
+        enableInterface(iface).expectResult(iface.name)
+        cb.expectAvailable()
+    }
+
+    @Test
+    fun testUpdateConfiguration_forBothIpConfigAndCapabilities() {
+        val iface = createInterface()
+        val cb = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface.name))
+        val network = cb.expectAvailable()
+        cb.assertNeverLost()
+
+        val testCapability = NET_CAPABILITY_TEMPORARILY_NOT_METERED
+        val nc = NetworkCapabilities
+                .Builder(ETH_REQUEST.networkCapabilities)
+                .addCapability(testCapability)
+                .build()
+        updateConfiguration(iface, STATIC_IP_CONFIGURATION, nc)
+
+        // UpdateConfiguration() currently does a restarts on the ethernet interface therefore lost
+        // will be expected first before available, as part of the restart.
+        cb.expectLost(network)
+        cb.expectAvailable()
+        cb.expectCapabilitiesWithCapability(testCapability)
+        cb.expectLinkPropertiesWithLinkAddress(
+                STATIC_IP_CONFIGURATION.staticIpConfiguration.ipAddress!!)
+    }
+
+    @Test
+    fun testUpdateConfiguration_forOnlyIpConfig() {
+        val iface: EthernetTestInterface = createInterface()
+        val cb = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface.name))
+        val network = cb.expectAvailable()
+        cb.assertNeverLost()
+
+        updateConfiguration(iface, STATIC_IP_CONFIGURATION)
+
+        // UpdateConfiguration() currently does a restarts on the ethernet interface therefore lost
+        // will be expected first before available, as part of the restart.
+        cb.expectLost(network)
+        cb.expectAvailable()
+        cb.expectCallback<CapabilitiesChanged>()
+        cb.expectLinkPropertiesWithLinkAddress(
+                STATIC_IP_CONFIGURATION.staticIpConfiguration.ipAddress!!)
+    }
+
+    @Test
+    fun testUpdateConfiguration_forOnlyCapabilities() {
+        val iface: EthernetTestInterface = createInterface()
+        val cb = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface.name))
+        val network = cb.expectAvailable()
+        cb.assertNeverLost()
+
+        val testCapability = NET_CAPABILITY_TEMPORARILY_NOT_METERED
+        val nc = NetworkCapabilities
+                .Builder(ETH_REQUEST.networkCapabilities)
+                .addCapability(testCapability)
+                .build()
+        updateConfiguration(iface, capabilities = nc)
+
+        // UpdateConfiguration() currently does a restarts on the ethernet interface therefore lost
+        // will be expected first before available, as part of the restart.
+        cb.expectLost(network)
+        cb.expectAvailable()
+        cb.expectCapabilitiesWithCapability(testCapability)
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt b/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt
index 462c8a3..375bfb8 100644
--- a/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt
@@ -17,9 +17,9 @@
 package android.net.cts
 
 import android.Manifest.permission.WRITE_DEVICE_CONFIG
-import android.net.util.NetworkStackUtils
 import android.provider.DeviceConfig
 import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
+import com.android.net.module.util.NetworkStackConstants
 import com.android.testutils.runAsShell
 
 /**
@@ -35,41 +35,41 @@
     @JvmStatic fun clearValidationTestUrlsDeviceConfig() {
         runAsShell(WRITE_DEVICE_CONFIG) {
             DeviceConfig.setProperty(NAMESPACE_CONNECTIVITY,
-                    NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL, null, false)
+                    NetworkStackConstants.TEST_CAPTIVE_PORTAL_HTTPS_URL, null, false)
             DeviceConfig.setProperty(NAMESPACE_CONNECTIVITY,
-                    NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL, null, false)
+                    NetworkStackConstants.TEST_CAPTIVE_PORTAL_HTTP_URL, null, false)
             DeviceConfig.setProperty(NAMESPACE_CONNECTIVITY,
-                    NetworkStackUtils.TEST_URL_EXPIRATION_TIME, null, false)
+                    NetworkStackConstants.TEST_URL_EXPIRATION_TIME, null, false)
         }
     }
 
     /**
      * Set the test validation HTTPS URL.
      *
-     * @see NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL
+     * @see NetworkStackConstants.TEST_CAPTIVE_PORTAL_HTTPS_URL
      */
     @JvmStatic
     fun setHttpsUrlDeviceConfig(rule: DeviceConfigRule, url: String?) =
             rule.setConfig(NAMESPACE_CONNECTIVITY,
-                NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL, url)
+                NetworkStackConstants.TEST_CAPTIVE_PORTAL_HTTPS_URL, url)
 
     /**
      * Set the test validation HTTP URL.
      *
-     * @see NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL
+     * @see NetworkStackConstants.TEST_CAPTIVE_PORTAL_HTTP_URL
      */
     @JvmStatic
     fun setHttpUrlDeviceConfig(rule: DeviceConfigRule, url: String?) =
             rule.setConfig(NAMESPACE_CONNECTIVITY,
-                NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL, url)
+                NetworkStackConstants.TEST_CAPTIVE_PORTAL_HTTP_URL, url)
 
     /**
      * Set the test validation URL expiration.
      *
-     * @see NetworkStackUtils.TEST_URL_EXPIRATION_TIME
+     * @see NetworkStackConstants.TEST_URL_EXPIRATION_TIME
      */
     @JvmStatic
     fun setUrlExpirationDeviceConfig(rule: DeviceConfigRule, timestamp: Long?) =
             rule.setConfig(NAMESPACE_CONNECTIVITY,
-                NetworkStackUtils.TEST_URL_EXPIRATION_TIME, timestamp?.toString())
+                NetworkStackConstants.TEST_URL_EXPIRATION_TIME, timestamp?.toString())
 }
diff --git a/tests/cts/tethering/Android.bp b/tests/cts/tethering/Android.bp
index 6096a8b..42949a4 100644
--- a/tests/cts/tethering/Android.bp
+++ b/tests/cts/tethering/Android.bp
@@ -53,10 +53,12 @@
 // mainline modules on release devices.
 android_test {
     name: "CtsTetheringTestLatestSdk",
-    defaults: ["CtsTetheringTestDefaults"],
+    defaults: [
+        "ConnectivityTestsLatestSdkDefaults",
+        "CtsTetheringTestDefaults",
+    ],
 
     min_sdk_version: "30",
-    target_sdk_version: "33",
 
     static_libs: [
         "TetheringIntegrationTestsLatestSdkLib",
diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp
index b3684ac..e3d80a0 100644
--- a/tests/integration/Android.bp
+++ b/tests/integration/Android.bp
@@ -21,7 +21,7 @@
 
 android_test {
     name: "FrameworksNetIntegrationTests",
-    defaults: ["framework-connectivity-test-defaults"],
+    defaults: ["framework-connectivity-internal-test-defaults"],
     platform_apis: true,
     certificate: "platform",
     srcs: [
@@ -71,8 +71,12 @@
         "net-tests-utils",
     ],
     libs: [
-        "service-connectivity-for-tests",
+        "service-connectivity-pre-jarjar",
         "services.core",
         "services.net",
     ],
+    visibility: [
+        "//packages/modules/Connectivity/tests/integration",
+        "//packages/modules/Connectivity/tests/unit",
+    ],
 }
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 80338aa..efc24d3 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
@@ -47,6 +47,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.connectivity.resources.R
+import com.android.server.BpfNetMaps
 import com.android.server.ConnectivityService
 import com.android.server.NetworkAgentWrapper
 import com.android.server.TestNetIdManager
@@ -208,6 +209,7 @@
         doReturn(mock(ProxyTracker::class.java)).`when`(deps).makeProxyTracker(any(), any())
         doReturn(mock(MockableSystemProperties::class.java)).`when`(deps).systemProperties
         doReturn(TestNetIdManager()).`when`(deps).makeNetIdManager()
+        doReturn(mock(BpfNetMaps::class.java)).`when`(deps).getBpfNetMaps(any())
         doAnswer { inv ->
             object : MultinetworkPolicyTracker(inv.getArgument(0), inv.getArgument(1),
                     inv.getArgument(2)) {
diff --git a/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt b/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt
index c7cf040..361c968 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt
@@ -22,8 +22,8 @@
 import android.net.INetworkMonitorCallbacks
 import android.net.Network
 import android.net.metrics.IpConnectivityLog
-import android.net.util.SharedLog
 import android.os.IBinder
+import com.android.net.module.util.SharedLog
 import com.android.networkstack.netlink.TcpSocketTracker
 import com.android.server.NetworkStackService
 import com.android.server.NetworkStackService.NetworkMonitorConnector
diff --git a/tests/mts/bpf_existence_test.cpp b/tests/mts/bpf_existence_test.cpp
index db39e6f..67b4f42 100644
--- a/tests/mts/bpf_existence_test.cpp
+++ b/tests/mts/bpf_existence_test.cpp
@@ -31,15 +31,11 @@
 using std::set;
 using std::string;
 
+using android::bpf::isAtLeastKernelVersion;
 using android::modules::sdklevel::IsAtLeastR;
 using android::modules::sdklevel::IsAtLeastS;
 using android::modules::sdklevel::IsAtLeastT;
 
-// Mainline development branches lack the constant for the current development OS.
-#ifndef __ANDROID_API_T__
-#define __ANDROID_API_T__ 33
-#endif
-
 #define PLATFORM "/sys/fs/bpf/"
 #define TETHERING "/sys/fs/bpf/tethering/"
 #define PRIVATE "/sys/fs/bpf/net_private/"
@@ -49,7 +45,8 @@
 class BpfExistenceTest : public ::testing::Test {
 };
 
-static const set<string> INTRODUCED_R = {
+// Part of Android R platform, but mainlined in S
+static const set<string> PLATFORM_ONLY_IN_R = {
     PLATFORM "map_offload_tether_ingress_map",
     PLATFORM "map_offload_tether_limit_map",
     PLATFORM "map_offload_tether_stats_map",
@@ -57,7 +54,8 @@
     PLATFORM "prog_offload_schedcls_ingress_tether_rawip",
 };
 
-static const set<string> INTRODUCED_S = {
+// Provided by *current* mainline module for S+ devices
+static const set<string> MAINLINE_FOR_S_PLUS = {
     TETHERING "map_offload_tether_dev_map",
     TETHERING "map_offload_tether_downstream4_map",
     TETHERING "map_offload_tether_downstream64_map",
@@ -67,6 +65,7 @@
     TETHERING "map_offload_tether_stats_map",
     TETHERING "map_offload_tether_upstream4_map",
     TETHERING "map_offload_tether_upstream6_map",
+    TETHERING "map_test_bitmap",
     TETHERING "map_test_tether_downstream6_map",
     TETHERING "prog_offload_schedcls_tether_downstream4_ether",
     TETHERING "prog_offload_schedcls_tether_downstream4_rawip",
@@ -78,25 +77,23 @@
     TETHERING "prog_offload_schedcls_tether_upstream6_rawip",
 };
 
-static const set<string> REMOVED_S = {
-    PLATFORM "map_offload_tether_ingress_map",
-    PLATFORM "map_offload_tether_limit_map",
-    PLATFORM "map_offload_tether_stats_map",
-    PLATFORM "prog_offload_schedcls_ingress_tether_ether",
-    PLATFORM "prog_offload_schedcls_ingress_tether_rawip",
+// Provided by *current* mainline module for S+ devices with 5.10+ kernels
+static const set<string> MAINLINE_FOR_S_5_10_PLUS = {
+    TETHERING "prog_test_xdp_drop_ipv4_udp_ether",
 };
 
-static const set<string> INTRODUCED_T = {
+// Provided by *current* mainline module for T+ devices
+static const set<string> MAINLINE_FOR_T_PLUS = {
     SHARED "map_block_blocked_ports_map",
     SHARED "map_clatd_clat_egress4_map",
     SHARED "map_clatd_clat_ingress6_map",
-    SHARED "map_dscp_policy_ipv4_dscp_policies_map",
-    SHARED "map_dscp_policy_ipv4_socket_to_policies_map_A",
-    SHARED "map_dscp_policy_ipv4_socket_to_policies_map_B",
-    SHARED "map_dscp_policy_ipv6_dscp_policies_map",
-    SHARED "map_dscp_policy_ipv6_socket_to_policies_map_A",
-    SHARED "map_dscp_policy_ipv6_socket_to_policies_map_B",
-    SHARED "map_dscp_policy_switch_comp_map",
+    SHARED "map_dscpPolicy_ipv4_dscp_policies_map",
+    SHARED "map_dscpPolicy_ipv4_socket_to_policies_map_A",
+    SHARED "map_dscpPolicy_ipv4_socket_to_policies_map_B",
+    SHARED "map_dscpPolicy_ipv6_dscp_policies_map",
+    SHARED "map_dscpPolicy_ipv6_socket_to_policies_map_A",
+    SHARED "map_dscpPolicy_ipv6_socket_to_policies_map_B",
+    SHARED "map_dscpPolicy_switch_comp_map",
     NETD "map_netd_app_uid_stats_map",
     NETD "map_netd_configuration_map",
     NETD "map_netd_cookie_tag_map",
@@ -121,58 +118,47 @@
     NETD "prog_netd_skfilter_ingress_xtbpf",
 };
 
-static const set<string> INTRODUCED_T_5_4 = {
+// Provided by *current* mainline module for T+ devices with 5.4+ kernels
+static const set<string> MAINLINE_FOR_T_5_4_PLUS = {
     SHARED "prog_block_bind4_block_port",
     SHARED "prog_block_bind6_block_port",
-    SHARED "prog_dscp_policy_schedcls_set_dscp_ether",
-    SHARED "prog_dscp_policy_schedcls_set_dscp_raw_ip",
 };
 
-static const set<string> REMOVED_T = {
+// Provided by *current* mainline module for T+ devices with 5.15+ kernels
+static const set<string> MAINLINE_FOR_T_5_15_PLUS = {
+    SHARED "prog_dscpPolicy_schedcls_set_dscp_ether",
+    SHARED "prog_dscpPolicy_schedcls_set_dscp_raw_ip",
 };
 
 void addAll(set<string>* a, const set<string>& b) {
     a->insert(b.begin(), b.end());
 }
 
-void removeAll(set<string>* a, const set<string>& b) {
-    for (const auto& toRemove : b) {
-        a->erase(toRemove);
-    }
-}
+#define DO_EXPECT(B, V) do { \
+    if (B) addAll(expected, (V)); else addAll(unexpected, (V)); \
+} while (0)
 
 void getFileLists(set<string>* expected, set<string>* unexpected) {
     unexpected->clear();
     expected->clear();
 
-    addAll(unexpected, INTRODUCED_R);
-    addAll(unexpected, INTRODUCED_S);
-    addAll(unexpected, INTRODUCED_T);
+    // We do not actually check the platform P/Q (netd) and Q (clatd) things
+    // and only verify the mainline module relevant R+ offload maps & progs.
+    //
+    // The goal of this test is to verify compatibility with the tethering mainline module,
+    // and not to test the platform itself, which may have been modified by vendor or oems,
+    // so we should only test for the removal of stuff that was mainline'd,
+    // and for the presence of mainline stuff.
+    DO_EXPECT(IsAtLeastR() && !IsAtLeastS(), PLATFORM_ONLY_IN_R);
 
-    if (IsAtLeastR()) {
-        addAll(expected, INTRODUCED_R);
-        removeAll(unexpected, INTRODUCED_R);
-        // Nothing removed in R.
-    }
-
-    if (IsAtLeastS()) {
-        addAll(expected, INTRODUCED_S);
-        removeAll(expected, REMOVED_S);
-
-        addAll(unexpected, REMOVED_S);
-        removeAll(unexpected, INTRODUCED_S);
-    }
+    DO_EXPECT(IsAtLeastS(), MAINLINE_FOR_S_PLUS);
+    DO_EXPECT(IsAtLeastS() && isAtLeastKernelVersion(5, 10, 0), MAINLINE_FOR_S_5_10_PLUS);
 
     // Nothing added or removed in SCv2.
 
-    if (IsAtLeastT()) {
-        addAll(expected, INTRODUCED_T);
-        if (android::bpf::isAtLeastKernelVersion(5, 4, 0)) addAll(expected, INTRODUCED_T_5_4);
-        removeAll(expected, REMOVED_T);
-
-        addAll(unexpected, REMOVED_T);
-        removeAll(unexpected, INTRODUCED_T);
-    }
+    DO_EXPECT(IsAtLeastT(), MAINLINE_FOR_T_PLUS);
+    DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(5, 4, 0), MAINLINE_FOR_T_5_4_PLUS);
+    DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(5, 15, 0), MAINLINE_FOR_T_5_15_PLUS);
 }
 
 void checkFiles() {
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 9d746b5..0908ad2 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -63,6 +63,7 @@
         "java/com/android/internal/net/NetworkUtilsInternalTest.java",
         "java/com/android/internal/net/VpnProfileTest.java",
         "java/com/android/server/NetworkManagementServiceTest.java",
+        "java/com/android/server/VpnManagerServiceTest.java",
         "java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java",
         "java/com/android/server/connectivity/IpConnectivityMetricsTest.java",
         "java/com/android/server/connectivity/MultipathPolicyTrackerTest.java",
diff --git a/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java b/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java
index b1b76ec..71c03ff 100644
--- a/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java
+++ b/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java
@@ -16,6 +16,10 @@
 
 package android.app.usage;
 
+import static android.net.NetworkStats.METERED_YES;
+import static android.net.NetworkTemplate.MATCH_MOBILE;
+import static android.net.NetworkTemplate.MATCH_WIFI;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
@@ -52,6 +56,8 @@
 import org.mockito.MockitoAnnotations;
 import org.mockito.invocation.InvocationOnMock;
 
+import java.util.Set;
+
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
@@ -204,20 +210,20 @@
     @Test
     public void testNetworkTemplateWhenRunningQueryDetails_NoSubscriberId() throws RemoteException {
         runQueryDetailsAndCheckTemplate(ConnectivityManager.TYPE_MOBILE,
-                null /* subscriberId */, NetworkTemplate.buildTemplateMobileWildcard());
+                null /* subscriberId */, new NetworkTemplate.Builder(MATCH_MOBILE)
+                        .setMeteredness(METERED_YES).build());
         runQueryDetailsAndCheckTemplate(ConnectivityManager.TYPE_WIFI,
-                "" /* subscriberId */, NetworkTemplate.buildTemplateWifiWildcard());
+                "" /* subscriberId */, new NetworkTemplate.Builder(MATCH_WIFI).build());
         runQueryDetailsAndCheckTemplate(ConnectivityManager.TYPE_WIFI,
-                null /* subscriberId */, NetworkTemplate.buildTemplateWifiWildcard());
+                null /* subscriberId */, new NetworkTemplate.Builder(MATCH_WIFI).build());
     }
 
     @Test
     public void testNetworkTemplateWhenRunningQueryDetails_MergedCarrierWifi()
             throws RemoteException {
         runQueryDetailsAndCheckTemplate(ConnectivityManager.TYPE_WIFI,
-                TEST_SUBSCRIBER_ID,
-                NetworkTemplate.buildTemplateWifi(NetworkTemplate.WIFI_NETWORKID_ALL,
-                        TEST_SUBSCRIBER_ID));
+                TEST_SUBSCRIBER_ID, new NetworkTemplate.Builder(MATCH_WIFI)
+                        .setSubscriberIds(Set.of(TEST_SUBSCRIBER_ID)).build());
     }
 
     @Test
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index 99e7ecc..0718952 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -24,13 +24,26 @@
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_POWERSAVE;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_RESTRICTED;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_STANDBY;
+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 com.android.server.BpfNetMaps.DOZABLE_MATCH;
+import static com.android.server.BpfNetMaps.HAPPY_BOX_MATCH;
+import static com.android.server.BpfNetMaps.IIF_MATCH;
+import static com.android.server.BpfNetMaps.LOCKDOWN_VPN_MATCH;
+import static com.android.server.BpfNetMaps.NO_MATCH;
+import static com.android.server.BpfNetMaps.PENALTY_BOX_MATCH;
+import static com.android.server.BpfNetMaps.POWERSAVE_MATCH;
+import static com.android.server.BpfNetMaps.RESTRICTED_MATCH;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeFalse;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.verify;
 
 import android.net.INetd;
@@ -68,7 +81,10 @@
 
     private static final int TEST_UID = 10086;
     private static final int[] TEST_UIDS = {10002, 10003};
-    private static final String IFNAME = "wlan0";
+    private static final String TEST_IF_NAME = "wlan0";
+    private static final int TEST_IF_INDEX = 7;
+    private static final int NO_IIF = 0;
+    private static final int NULL_IIF = 0;
     private static final String CHAINNAME = "fw_dozable";
     private static final U32 UID_RULES_CONFIGURATION_KEY = new U32(0);
     private static final List<Integer> FIREWALL_CHAINS = List.of(
@@ -85,108 +101,98 @@
     private BpfNetMaps mBpfNetMaps;
 
     @Mock INetd mNetd;
-    private static final TestBpfMap<U32, U32> sConfigurationMap =
-            new TestBpfMap<>(U32.class, U32.class);
+    @Mock BpfNetMaps.Dependencies mDeps;
+    private final BpfMap<U32, U32> mConfigurationMap = new TestBpfMap<>(U32.class, U32.class);
+    private final BpfMap<U32, UidOwnerValue> mUidOwnerMap =
+            new TestBpfMap<>(U32.class, UidOwnerValue.class);
 
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        mBpfNetMaps = new BpfNetMaps(mNetd);
-        BpfNetMaps.initialize(makeDependencies());
-        sConfigurationMap.clear();
-    }
-
-    private static BpfNetMaps.Dependencies makeDependencies() {
-        return new BpfNetMaps.Dependencies() {
-            @Override
-            public BpfMap<U32, U32> getConfigurationMap() {
-                return sConfigurationMap;
-            }
-        };
+        doReturn(TEST_IF_INDEX).when(mDeps).getIfIndex(TEST_IF_NAME);
+        BpfNetMaps.setConfigurationMapForTest(mConfigurationMap);
+        BpfNetMaps.setUidOwnerMapForTest(mUidOwnerMap);
+        mBpfNetMaps = new BpfNetMaps(mNetd, mDeps);
     }
 
     @Test
     public void testBpfNetMapsBeforeT() throws Exception {
         assumeFalse(SdkLevel.isAtLeastT());
-        mBpfNetMaps.addUidInterfaceRules(IFNAME, TEST_UIDS);
-        verify(mNetd).firewallAddUidInterfaceRules(IFNAME, TEST_UIDS);
+        mBpfNetMaps.addUidInterfaceRules(TEST_IF_NAME, TEST_UIDS);
+        verify(mNetd).firewallAddUidInterfaceRules(TEST_IF_NAME, TEST_UIDS);
         mBpfNetMaps.removeUidInterfaceRules(TEST_UIDS);
         verify(mNetd).firewallRemoveUidInterfaceRules(TEST_UIDS);
         mBpfNetMaps.setNetPermForUids(PERMISSION_INTERNET, TEST_UIDS);
         verify(mNetd).trafficSetNetPermForUids(PERMISSION_INTERNET, TEST_UIDS);
     }
 
-    private void doTestGetChainEnabled(final List<Integer> enableChains) throws Exception {
+    private long getMatch(final List<Integer> chains) {
         long match = 0;
-        for (final int chain: enableChains) {
+        for (final int chain: chains) {
             match |= mBpfNetMaps.getMatchByFirewallChain(chain);
         }
-        sConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(match));
+        return match;
+    }
+
+    private void doTestIsChainEnabled(final List<Integer> enableChains) throws Exception {
+        mConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(getMatch(enableChains)));
 
         for (final int chain: FIREWALL_CHAINS) {
             final String testCase = "EnabledChains: " + enableChains + " CheckedChain: " + chain;
             if (enableChains.contains(chain)) {
-                assertTrue("Expected getChainEnabled returns True, " + testCase,
-                        mBpfNetMaps.getChainEnabled(chain));
+                assertTrue("Expected isChainEnabled returns True, " + testCase,
+                        mBpfNetMaps.isChainEnabled(chain));
             } else {
-                assertFalse("Expected getChainEnabled returns False, " + testCase,
-                        mBpfNetMaps.getChainEnabled(chain));
+                assertFalse("Expected isChainEnabled returns False, " + testCase,
+                        mBpfNetMaps.isChainEnabled(chain));
             }
         }
     }
 
-    private void doTestGetChainEnabled(final int enableChain) throws Exception {
-        doTestGetChainEnabled(List.of(enableChain));
+    private void doTestIsChainEnabled(final int enableChain) throws Exception {
+        doTestIsChainEnabled(List.of(enableChain));
     }
 
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.S_V2)
-    public void testGetChainEnabled() throws Exception {
-        doTestGetChainEnabled(FIREWALL_CHAIN_DOZABLE);
-        doTestGetChainEnabled(FIREWALL_CHAIN_STANDBY);
-        doTestGetChainEnabled(FIREWALL_CHAIN_POWERSAVE);
-        doTestGetChainEnabled(FIREWALL_CHAIN_RESTRICTED);
-        doTestGetChainEnabled(FIREWALL_CHAIN_LOW_POWER_STANDBY);
-        doTestGetChainEnabled(FIREWALL_CHAIN_OEM_DENY_1);
-        doTestGetChainEnabled(FIREWALL_CHAIN_OEM_DENY_2);
-        doTestGetChainEnabled(FIREWALL_CHAIN_OEM_DENY_3);
+    public void testIsChainEnabled() throws Exception {
+        doTestIsChainEnabled(FIREWALL_CHAIN_DOZABLE);
+        doTestIsChainEnabled(FIREWALL_CHAIN_STANDBY);
+        doTestIsChainEnabled(FIREWALL_CHAIN_POWERSAVE);
+        doTestIsChainEnabled(FIREWALL_CHAIN_RESTRICTED);
+        doTestIsChainEnabled(FIREWALL_CHAIN_LOW_POWER_STANDBY);
+        doTestIsChainEnabled(FIREWALL_CHAIN_OEM_DENY_1);
+        doTestIsChainEnabled(FIREWALL_CHAIN_OEM_DENY_2);
+        doTestIsChainEnabled(FIREWALL_CHAIN_OEM_DENY_3);
     }
 
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.S_V2)
-    public void testGetChainEnabledMultipleChainEnabled() throws Exception {
-        doTestGetChainEnabled(List.of(
+    public void testIsChainEnabledMultipleChainEnabled() throws Exception {
+        doTestIsChainEnabled(List.of(
                 FIREWALL_CHAIN_DOZABLE,
                 FIREWALL_CHAIN_STANDBY));
-        doTestGetChainEnabled(List.of(
+        doTestIsChainEnabled(List.of(
                 FIREWALL_CHAIN_DOZABLE,
                 FIREWALL_CHAIN_STANDBY,
                 FIREWALL_CHAIN_POWERSAVE,
                 FIREWALL_CHAIN_RESTRICTED));
-        doTestGetChainEnabled(FIREWALL_CHAINS);
+        doTestIsChainEnabled(FIREWALL_CHAINS);
     }
 
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.S_V2)
-    public void testGetChainEnabledInvalidChain() {
+    public void testIsChainEnabledInvalidChain() {
         final Class<ServiceSpecificException> expected = ServiceSpecificException.class;
-        assertThrows(expected, () -> mBpfNetMaps.getChainEnabled(-1 /* childChain */));
-        assertThrows(expected, () -> mBpfNetMaps.getChainEnabled(1000 /* childChain */));
-    }
-
-    @Test
-    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
-    public void testGetChainEnabledMissingConfiguration() {
-        // sConfigurationMap does not have entry for UID_RULES_CONFIGURATION_KEY
-        assertThrows(ServiceSpecificException.class,
-                () -> mBpfNetMaps.getChainEnabled(FIREWALL_CHAIN_DOZABLE));
+        assertThrows(expected, () -> mBpfNetMaps.isChainEnabled(-1 /* childChain */));
+        assertThrows(expected, () -> mBpfNetMaps.isChainEnabled(1000 /* childChain */));
     }
 
     @Test
     @IgnoreAfter(Build.VERSION_CODES.S_V2)
-    public void testGetChainEnabledBeforeT() {
+    public void testIsChainEnabledBeforeT() {
         assertThrows(UnsupportedOperationException.class,
-                () -> mBpfNetMaps.getChainEnabled(FIREWALL_CHAIN_DOZABLE));
+                () -> mBpfNetMaps.isChainEnabled(FIREWALL_CHAIN_DOZABLE));
     }
 
     private void doTestSetChildChain(final List<Integer> testChains) throws Exception {
@@ -195,17 +201,17 @@
             expectedMatch |= mBpfNetMaps.getMatchByFirewallChain(chain);
         }
 
-        assertEquals(0, sConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY).val);
+        assertEquals(0, mConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY).val);
 
         for (final int chain: testChains) {
             mBpfNetMaps.setChildChain(chain, true /* enable */);
         }
-        assertEquals(expectedMatch, sConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY).val);
+        assertEquals(expectedMatch, mConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY).val);
 
         for (final int chain: testChains) {
             mBpfNetMaps.setChildChain(chain, false /* enable */);
         }
-        assertEquals(0, sConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY).val);
+        assertEquals(0, mConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY).val);
     }
 
     private void doTestSetChildChain(final int testChain) throws Exception {
@@ -215,7 +221,7 @@
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.S_V2)
     public void testSetChildChain() throws Exception {
-        sConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(0));
+        mConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(0));
         doTestSetChildChain(FIREWALL_CHAIN_DOZABLE);
         doTestSetChildChain(FIREWALL_CHAIN_STANDBY);
         doTestSetChildChain(FIREWALL_CHAIN_POWERSAVE);
@@ -229,7 +235,7 @@
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.S_V2)
     public void testSetChildChainMultipleChain() throws Exception {
-        sConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(0));
+        mConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(0));
         doTestSetChildChain(List.of(
                 FIREWALL_CHAIN_DOZABLE,
                 FIREWALL_CHAIN_STANDBY));
@@ -252,17 +258,395 @@
     }
 
     @Test
-    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
-    public void testSetChildChainMissingConfiguration() {
-        // sConfigurationMap does not have entry for UID_RULES_CONFIGURATION_KEY
-        assertThrows(ServiceSpecificException.class,
-                () -> mBpfNetMaps.setChildChain(FIREWALL_CHAIN_DOZABLE, true /* enable */));
-    }
-
-    @Test
     @IgnoreAfter(Build.VERSION_CODES.S_V2)
     public void testSetChildChainBeforeT() {
         assertThrows(UnsupportedOperationException.class,
                 () -> mBpfNetMaps.setChildChain(FIREWALL_CHAIN_DOZABLE, true /* enable */));
     }
+
+    private void checkUidOwnerValue(final long uid, final long expectedIif,
+            final long expectedMatch) throws Exception {
+        final UidOwnerValue config = mUidOwnerMap.getValue(new U32(uid));
+        if (expectedMatch == 0) {
+            assertNull(config);
+        } else {
+            assertEquals(expectedIif, config.iif);
+            assertEquals(expectedMatch, config.rule);
+        }
+    }
+
+    private void doTestRemoveNaughtyApp(final long iif, final long match) throws Exception {
+        mUidOwnerMap.updateEntry(new U32(TEST_UID), new UidOwnerValue(iif, match));
+
+        mBpfNetMaps.removeNaughtyApp(TEST_UID);
+
+        checkUidOwnerValue(TEST_UID, iif, match & ~PENALTY_BOX_MATCH);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testRemoveNaughtyApp() throws Exception {
+        doTestRemoveNaughtyApp(NO_IIF, PENALTY_BOX_MATCH);
+
+        // PENALTY_BOX_MATCH with other matches
+        doTestRemoveNaughtyApp(NO_IIF, PENALTY_BOX_MATCH | DOZABLE_MATCH | POWERSAVE_MATCH);
+
+        // PENALTY_BOX_MATCH with IIF_MATCH
+        doTestRemoveNaughtyApp(TEST_IF_INDEX, PENALTY_BOX_MATCH | IIF_MATCH);
+
+        // PENALTY_BOX_MATCH is not enabled
+        doTestRemoveNaughtyApp(NO_IIF, DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testRemoveNaughtyAppMissingUid() {
+        // UidOwnerMap does not have entry for TEST_UID
+        assertThrows(ServiceSpecificException.class,
+                () -> mBpfNetMaps.removeNaughtyApp(TEST_UID));
+    }
+
+    @Test
+    @IgnoreAfter(Build.VERSION_CODES.S_V2)
+    public void testRemoveNaughtyAppBeforeT() {
+        assertThrows(UnsupportedOperationException.class,
+                () -> mBpfNetMaps.removeNaughtyApp(TEST_UID));
+    }
+
+    private void doTestAddNaughtyApp(final long iif, final long match) throws Exception {
+        if (match != NO_MATCH) {
+            mUidOwnerMap.updateEntry(new U32(TEST_UID), new UidOwnerValue(iif, match));
+        }
+
+        mBpfNetMaps.addNaughtyApp(TEST_UID);
+
+        checkUidOwnerValue(TEST_UID, iif, match | PENALTY_BOX_MATCH);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testAddNaughtyApp() throws Exception {
+        doTestAddNaughtyApp(NO_IIF, NO_MATCH);
+
+        // Other matches are enabled
+        doTestAddNaughtyApp(NO_IIF, DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH);
+
+        // IIF_MATCH is enabled
+        doTestAddNaughtyApp(TEST_IF_INDEX, IIF_MATCH);
+
+        // PENALTY_BOX_MATCH is already enabled
+        doTestAddNaughtyApp(NO_IIF, PENALTY_BOX_MATCH | DOZABLE_MATCH);
+    }
+
+    @Test
+    @IgnoreAfter(Build.VERSION_CODES.S_V2)
+    public void testAddNaughtyAppBeforeT() {
+        assertThrows(UnsupportedOperationException.class,
+                () -> mBpfNetMaps.addNaughtyApp(TEST_UID));
+    }
+
+    private void doTestRemoveNiceApp(final long iif, final long match) throws Exception {
+        mUidOwnerMap.updateEntry(new U32(TEST_UID), new UidOwnerValue(iif, match));
+
+        mBpfNetMaps.removeNiceApp(TEST_UID);
+
+        checkUidOwnerValue(TEST_UID, iif, match & ~HAPPY_BOX_MATCH);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testRemoveNiceApp() throws Exception {
+        doTestRemoveNiceApp(NO_IIF, HAPPY_BOX_MATCH);
+
+        // HAPPY_BOX_MATCH with other matches
+        doTestRemoveNiceApp(NO_IIF, HAPPY_BOX_MATCH | DOZABLE_MATCH | POWERSAVE_MATCH);
+
+        // HAPPY_BOX_MATCH with IIF_MATCH
+        doTestRemoveNiceApp(TEST_IF_INDEX, HAPPY_BOX_MATCH | IIF_MATCH);
+
+        // HAPPY_BOX_MATCH is not enabled
+        doTestRemoveNiceApp(NO_IIF, DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testRemoveNiceAppMissingUid() {
+        // UidOwnerMap does not have entry for TEST_UID
+        assertThrows(ServiceSpecificException.class,
+                () -> mBpfNetMaps.removeNiceApp(TEST_UID));
+    }
+
+    @Test
+    @IgnoreAfter(Build.VERSION_CODES.S_V2)
+    public void testRemoveNiceAppBeforeT() {
+        assertThrows(UnsupportedOperationException.class,
+                () -> mBpfNetMaps.removeNiceApp(TEST_UID));
+    }
+
+    private void doTestAddNiceApp(final long iif, final long match) throws Exception {
+        if (match != NO_MATCH) {
+            mUidOwnerMap.updateEntry(new U32(TEST_UID), new UidOwnerValue(iif, match));
+        }
+
+        mBpfNetMaps.addNiceApp(TEST_UID);
+
+        checkUidOwnerValue(TEST_UID, iif, match | HAPPY_BOX_MATCH);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testAddNiceApp() throws Exception {
+        doTestAddNiceApp(NO_IIF, NO_MATCH);
+
+        // Other matches are enabled
+        doTestAddNiceApp(NO_IIF, DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH);
+
+        // IIF_MATCH is enabled
+        doTestAddNiceApp(TEST_IF_INDEX, IIF_MATCH);
+
+        // HAPPY_BOX_MATCH is already enabled
+        doTestAddNiceApp(NO_IIF, HAPPY_BOX_MATCH | DOZABLE_MATCH);
+    }
+
+    @Test
+    @IgnoreAfter(Build.VERSION_CODES.S_V2)
+    public void testAddNiceAppBeforeT() {
+        assertThrows(UnsupportedOperationException.class,
+                () -> mBpfNetMaps.addNiceApp(TEST_UID));
+    }
+
+    private void doTestUpdateUidLockdownRule(final long iif, final long match, final boolean add)
+            throws Exception {
+        if (match != NO_MATCH) {
+            mUidOwnerMap.updateEntry(new U32(TEST_UID), new UidOwnerValue(iif, match));
+        }
+
+        mBpfNetMaps.updateUidLockdownRule(TEST_UID, add);
+
+        final long expectedMatch = add ? match | LOCKDOWN_VPN_MATCH : match & ~LOCKDOWN_VPN_MATCH;
+        checkUidOwnerValue(TEST_UID, iif, expectedMatch);
+    }
+
+    private static final boolean ADD = true;
+    private static final boolean REMOVE = false;
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testUpdateUidLockdownRuleAddLockdown() throws Exception {
+        doTestUpdateUidLockdownRule(NO_IIF, NO_MATCH, ADD);
+
+        // Other matches are enabled
+        doTestUpdateUidLockdownRule(
+                NO_IIF, DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH, ADD);
+
+        // IIF_MATCH is enabled
+        doTestUpdateUidLockdownRule(TEST_IF_INDEX, DOZABLE_MATCH, ADD);
+
+        // LOCKDOWN_VPN_MATCH is already enabled
+        doTestUpdateUidLockdownRule(NO_IIF, LOCKDOWN_VPN_MATCH | DOZABLE_MATCH, ADD);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testUpdateUidLockdownRuleRemoveLockdown() throws Exception {
+        doTestUpdateUidLockdownRule(NO_IIF, LOCKDOWN_VPN_MATCH, REMOVE);
+
+        // LOCKDOWN_VPN_MATCH with other matches
+        doTestUpdateUidLockdownRule(
+                NO_IIF, LOCKDOWN_VPN_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH, REMOVE);
+
+        // LOCKDOWN_VPN_MATCH with IIF_MATCH
+        doTestUpdateUidLockdownRule(TEST_IF_INDEX, LOCKDOWN_VPN_MATCH | IIF_MATCH, REMOVE);
+
+        // LOCKDOWN_VPN_MATCH is not enabled
+        doTestUpdateUidLockdownRule(NO_IIF, POWERSAVE_MATCH | RESTRICTED_MATCH, REMOVE);
+    }
+
+    @Test
+    @IgnoreAfter(Build.VERSION_CODES.S_V2)
+    public void testUpdateUidLockdownRuleBeforeT() {
+        assertThrows(UnsupportedOperationException.class,
+                () -> mBpfNetMaps.updateUidLockdownRule(TEST_UID, true /* add */));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testAddUidInterfaceRules() throws Exception {
+        final int uid0 = TEST_UIDS[0];
+        final int uid1 = TEST_UIDS[1];
+
+        mBpfNetMaps.addUidInterfaceRules(TEST_IF_NAME, TEST_UIDS);
+
+        checkUidOwnerValue(uid0, TEST_IF_INDEX, IIF_MATCH);
+        checkUidOwnerValue(uid1, TEST_IF_INDEX, IIF_MATCH);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testAddUidInterfaceRulesWithOtherMatch() throws Exception {
+        final int uid0 = TEST_UIDS[0];
+        final int uid1 = TEST_UIDS[1];
+        final long match0 = DOZABLE_MATCH;
+        final long match1 = DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH;
+        mUidOwnerMap.updateEntry(new U32(uid0), new UidOwnerValue(NO_IIF, match0));
+        mUidOwnerMap.updateEntry(new U32(uid1), new UidOwnerValue(NO_IIF, match1));
+
+        mBpfNetMaps.addUidInterfaceRules(TEST_IF_NAME, TEST_UIDS);
+
+        checkUidOwnerValue(uid0, TEST_IF_INDEX, match0 | IIF_MATCH);
+        checkUidOwnerValue(uid1, TEST_IF_INDEX, match1 | IIF_MATCH);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testAddUidInterfaceRulesWithExistingIifMatch() throws Exception {
+        final int uid0 = TEST_UIDS[0];
+        final int uid1 = TEST_UIDS[1];
+        final long match0 = IIF_MATCH;
+        final long match1 = IIF_MATCH | DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH;
+        mUidOwnerMap.updateEntry(new U32(uid0), new UidOwnerValue(TEST_IF_INDEX + 1, match0));
+        mUidOwnerMap.updateEntry(new U32(uid1), new UidOwnerValue(NULL_IIF, match1));
+
+        mBpfNetMaps.addUidInterfaceRules(TEST_IF_NAME, TEST_UIDS);
+
+        checkUidOwnerValue(uid0, TEST_IF_INDEX, match0);
+        checkUidOwnerValue(uid1, TEST_IF_INDEX, match1);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testAddUidInterfaceRulesGetIfIndexFail() {
+        doReturn(0).when(mDeps).getIfIndex(TEST_IF_NAME);
+        assertThrows(ServiceSpecificException.class,
+                () -> mBpfNetMaps.addUidInterfaceRules(TEST_IF_NAME, TEST_UIDS));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testAddUidInterfaceRulesWithNullInterface() throws Exception {
+        final int uid0 = TEST_UIDS[0];
+        final int uid1 = TEST_UIDS[1];
+        final long match0 = IIF_MATCH;
+        final long match1 = IIF_MATCH | DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH;
+        mUidOwnerMap.updateEntry(new U32(uid0), new UidOwnerValue(TEST_IF_INDEX, match0));
+        mUidOwnerMap.updateEntry(new U32(uid1), new UidOwnerValue(NULL_IIF, match1));
+
+        mBpfNetMaps.addUidInterfaceRules(null /* ifName */, TEST_UIDS);
+
+        checkUidOwnerValue(uid0, NULL_IIF, match0);
+        checkUidOwnerValue(uid1, NULL_IIF, match1);
+    }
+
+    private void doTestRemoveUidInterfaceRules(final long iif0, final long match0,
+            final long iif1, final long match1) throws Exception {
+        final int uid0 = TEST_UIDS[0];
+        final int uid1 = TEST_UIDS[1];
+        mUidOwnerMap.updateEntry(new U32(uid0), new UidOwnerValue(iif0, match0));
+        mUidOwnerMap.updateEntry(new U32(uid1), new UidOwnerValue(iif1, match1));
+
+        mBpfNetMaps.removeUidInterfaceRules(TEST_UIDS);
+
+        checkUidOwnerValue(uid0, NO_IIF, match0 & ~IIF_MATCH);
+        checkUidOwnerValue(uid1, NO_IIF, match1 & ~IIF_MATCH);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testRemoveUidInterfaceRules() throws Exception {
+        doTestRemoveUidInterfaceRules(TEST_IF_INDEX, IIF_MATCH, NULL_IIF, IIF_MATCH);
+
+        // IIF_MATCH and other matches are enabled
+        doTestRemoveUidInterfaceRules(TEST_IF_INDEX, IIF_MATCH | DOZABLE_MATCH,
+                NULL_IIF, IIF_MATCH | DOZABLE_MATCH | RESTRICTED_MATCH);
+
+        // IIF_MATCH is not enabled
+        doTestRemoveUidInterfaceRules(NO_IIF, DOZABLE_MATCH,
+                NO_IIF, DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH);
+    }
+
+    private void doTestSetUidRule(final List<Integer> testChains) throws Exception {
+        mUidOwnerMap.updateEntry(new U32(TEST_UID), new UidOwnerValue(TEST_IF_INDEX, IIF_MATCH));
+
+        for (final int chain: testChains) {
+            final int ruleToAddMatch = mBpfNetMaps.isFirewallAllowList(chain)
+                    ? FIREWALL_RULE_ALLOW : FIREWALL_RULE_DENY;
+            mBpfNetMaps.setUidRule(chain, TEST_UID, ruleToAddMatch);
+        }
+
+        checkUidOwnerValue(TEST_UID, TEST_IF_INDEX, IIF_MATCH | getMatch(testChains));
+
+        for (final int chain: testChains) {
+            final int ruleToRemoveMatch = mBpfNetMaps.isFirewallAllowList(chain)
+                    ? FIREWALL_RULE_DENY : FIREWALL_RULE_ALLOW;
+            mBpfNetMaps.setUidRule(chain, TEST_UID, ruleToRemoveMatch);
+        }
+
+        checkUidOwnerValue(TEST_UID, TEST_IF_INDEX, IIF_MATCH);
+    }
+
+    private void doTestSetUidRule(final int testChain) throws Exception {
+        doTestSetUidRule(List.of(testChain));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testSetUidRule() throws Exception {
+        doTestSetUidRule(FIREWALL_CHAIN_DOZABLE);
+        doTestSetUidRule(FIREWALL_CHAIN_STANDBY);
+        doTestSetUidRule(FIREWALL_CHAIN_POWERSAVE);
+        doTestSetUidRule(FIREWALL_CHAIN_RESTRICTED);
+        doTestSetUidRule(FIREWALL_CHAIN_LOW_POWER_STANDBY);
+        doTestSetUidRule(FIREWALL_CHAIN_OEM_DENY_1);
+        doTestSetUidRule(FIREWALL_CHAIN_OEM_DENY_2);
+        doTestSetUidRule(FIREWALL_CHAIN_OEM_DENY_3);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testSetUidRuleMultipleChain() throws Exception {
+        doTestSetUidRule(List.of(
+                FIREWALL_CHAIN_DOZABLE,
+                FIREWALL_CHAIN_STANDBY));
+        doTestSetUidRule(List.of(
+                FIREWALL_CHAIN_DOZABLE,
+                FIREWALL_CHAIN_STANDBY,
+                FIREWALL_CHAIN_POWERSAVE,
+                FIREWALL_CHAIN_RESTRICTED));
+        doTestSetUidRule(FIREWALL_CHAINS);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testSetUidRuleRemoveRuleFromUidWithNoRule() {
+        final Class<ServiceSpecificException> expected = ServiceSpecificException.class;
+        assertThrows(expected,
+                () -> mBpfNetMaps.setUidRule(FIREWALL_CHAIN_DOZABLE, TEST_UID, FIREWALL_RULE_DENY));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testSetUidRuleInvalidChain() {
+        final Class<ServiceSpecificException> expected = ServiceSpecificException.class;
+        assertThrows(expected,
+                () -> mBpfNetMaps.setUidRule(-1 /* childChain */, TEST_UID, FIREWALL_RULE_ALLOW));
+        assertThrows(expected,
+                () -> mBpfNetMaps.setUidRule(1000 /* childChain */, TEST_UID, FIREWALL_RULE_ALLOW));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testSetUidRuleInvalidRule() {
+        final Class<ServiceSpecificException> expected = ServiceSpecificException.class;
+        assertThrows(expected, () ->
+                mBpfNetMaps.setUidRule(FIREWALL_CHAIN_DOZABLE, TEST_UID, -1 /* firewallRule */));
+        assertThrows(expected, () ->
+                mBpfNetMaps.setUidRule(FIREWALL_CHAIN_DOZABLE, TEST_UID, 1000 /* firewallRule */));
+    }
+
+    @Test
+    @IgnoreAfter(Build.VERSION_CODES.S_V2)
+    public void testSetUidRuleBeforeT() {
+        assertThrows(UnsupportedOperationException.class, () ->
+                mBpfNetMaps.setUidRule(FIREWALL_CHAIN_DOZABLE, TEST_UID, FIREWALL_RULE_ALLOW));
+    }
 }
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 900ee5a..0919dfc 100644
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -10633,19 +10633,6 @@
     }
 
     @Test
-    public void testStartVpnProfileFromDiffPackage() throws Exception {
-        final String notMyVpnPkg = "com.not.my.vpn";
-        assertThrows(
-                SecurityException.class, () -> mVpnManagerService.startVpnProfile(notMyVpnPkg));
-    }
-
-    @Test
-    public void testStopVpnProfileFromDiffPackage() throws Exception {
-        final String notMyVpnPkg = "com.not.my.vpn";
-        assertThrows(SecurityException.class, () -> mVpnManagerService.stopVpnProfile(notMyVpnPkg));
-    }
-
-    @Test
     public void testUidUpdateChangesInterfaceFilteringRule() throws Exception {
         LinkProperties lp = new LinkProperties();
         lp.setInterfaceName("tun0");
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 9365bee..07884cf 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server;
 
+import static android.net.nsd.NsdManager.FAILURE_INTERNAL_ERROR;
+
 import static libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
 import static libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
 
@@ -44,11 +46,15 @@
 import android.net.mdns.aidl.DiscoveryInfo;
 import android.net.mdns.aidl.GetAddressInfo;
 import android.net.mdns.aidl.IMDnsEventListener;
+import android.net.mdns.aidl.RegistrationInfo;
 import android.net.mdns.aidl.ResolutionInfo;
 import android.net.nsd.INsdManagerCallback;
 import android.net.nsd.INsdServiceConnector;
 import android.net.nsd.MDnsManager;
 import android.net.nsd.NsdManager;
+import android.net.nsd.NsdManager.DiscoveryListener;
+import android.net.nsd.NsdManager.RegistrationListener;
+import android.net.nsd.NsdManager.ResolveListener;
 import android.net.nsd.NsdServiceInfo;
 import android.os.Binder;
 import android.os.Build;
@@ -86,10 +92,15 @@
 @SmallTest
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class NsdServiceTest {
-
     static final int PROTOCOL = NsdManager.PROTOCOL_DNS_SD;
     private static final long CLEANUP_DELAY_MS = 500;
     private static final long TIMEOUT_MS = 500;
+    private static final String SERVICE_NAME = "a_name";
+    private static final String SERVICE_TYPE = "a_type";
+    private static final String SERVICE_FULL_NAME = SERVICE_NAME + "." + SERVICE_TYPE;
+    private static final String DOMAIN_NAME = "mytestdevice.local";
+    private static final int PORT = 2201;
+    private static final int IFACE_IDX_ANY = 0;
 
     // Records INsdManagerCallback created when NsdService#connect is called.
     // Only accessed on the test thread, since NsdService#connect is called by the NsdManager
@@ -103,6 +114,7 @@
     @Mock MDnsManager mMockMDnsM;
     HandlerThread mThread;
     TestHandler mHandler;
+    NsdService mService;
 
     private static class LinkToDeathRecorder extends Binder {
         IBinder.DeathRecipient mDr;
@@ -134,6 +146,8 @@
         doReturn(true).when(mMockMDnsM).discover(anyInt(), anyString(), anyInt());
         doReturn(true).when(mMockMDnsM).resolve(
                 anyInt(), anyString(), anyString(), anyString(), anyInt());
+
+        mService = makeService();
     }
 
     @After
@@ -147,18 +161,14 @@
     @Test
     @DisableCompatChanges(NsdManager.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS)
     public void testPreSClients() throws Exception {
-        NsdService service = makeService();
-
         // Pre S client connected, the daemon should be started.
-        connectClient(service);
-        waitForIdle();
+        connectClient(mService);
         final INsdManagerCallback cb1 = getCallback();
         final IBinder.DeathRecipient deathRecipient1 = verifyLinkToDeath(cb1);
         verify(mMockMDnsM, times(1)).registerEventListener(any());
         verify(mMockMDnsM, times(1)).startDaemon();
 
-        connectClient(service);
-        waitForIdle();
+        connectClient(mService);
         final INsdManagerCallback cb2 = getCallback();
         final IBinder.DeathRecipient deathRecipient2 = verifyLinkToDeath(cb2);
         // Daemon has been started, it should not try to start it again.
@@ -178,19 +188,15 @@
     @Test
     @EnableCompatChanges(NsdManager.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS)
     public void testNoDaemonStartedWhenClientsConnect() throws Exception {
-        final NsdService service = makeService();
-
         // Creating an NsdManager will not cause daemon startup.
-        connectClient(service);
-        waitForIdle();
+        connectClient(mService);
         verify(mMockMDnsM, never()).registerEventListener(any());
         verify(mMockMDnsM, never()).startDaemon();
         final INsdManagerCallback cb1 = getCallback();
         final IBinder.DeathRecipient deathRecipient1 = verifyLinkToDeath(cb1);
 
         // Creating another NsdManager will not cause daemon startup either.
-        connectClient(service);
-        waitForIdle();
+        connectClient(mService);
         verify(mMockMDnsM, never()).registerEventListener(any());
         verify(mMockMDnsM, never()).startDaemon();
         final INsdManagerCallback cb2 = getCallback();
@@ -216,70 +222,66 @@
     @Test
     @EnableCompatChanges(NsdManager.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS)
     public void testClientRequestsAreGCedAtDisconnection() throws Exception {
-        NsdService service = makeService();
-
-        NsdManager client = connectClient(service);
-        waitForIdle();
+        final NsdManager client = connectClient(mService);
         final INsdManagerCallback cb1 = getCallback();
         final IBinder.DeathRecipient deathRecipient = verifyLinkToDeath(cb1);
         verify(mMockMDnsM, never()).registerEventListener(any());
         verify(mMockMDnsM, never()).startDaemon();
 
-        NsdServiceInfo request = new NsdServiceInfo("a_name", "a_type");
-        request.setPort(2201);
+        final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
+        request.setPort(PORT);
 
         // Client registration request
-        NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class);
+        final RegistrationListener listener1 = mock(RegistrationListener.class);
         client.registerService(request, PROTOCOL, listener1);
         waitForIdle();
-        verify(mMockMDnsM, times(1)).registerEventListener(any());
-        verify(mMockMDnsM, times(1)).startDaemon();
-        verify(mMockMDnsM, times(1)).registerService(
-                eq(2), eq("a_name"), eq("a_type"), eq(2201), any(), eq(0));
+        verify(mMockMDnsM).registerEventListener(any());
+        verify(mMockMDnsM).startDaemon();
+        verify(mMockMDnsM).registerService(
+                eq(2), eq(SERVICE_NAME), eq(SERVICE_TYPE), eq(PORT), any(), eq(IFACE_IDX_ANY));
 
         // Client discovery request
-        NsdManager.DiscoveryListener listener2 = mock(NsdManager.DiscoveryListener.class);
-        client.discoverServices("a_type", PROTOCOL, listener2);
+        final DiscoveryListener listener2 = mock(DiscoveryListener.class);
+        client.discoverServices(SERVICE_TYPE, PROTOCOL, listener2);
         waitForIdle();
-        verify(mMockMDnsM, times(1)).discover(eq(3), eq("a_type"), eq(0));
+        verify(mMockMDnsM).discover(3 /* id */, SERVICE_TYPE, IFACE_IDX_ANY);
 
         // Client resolve request
-        NsdManager.ResolveListener listener3 = mock(NsdManager.ResolveListener.class);
+        final ResolveListener listener3 = mock(ResolveListener.class);
         client.resolveService(request, listener3);
         waitForIdle();
-        verify(mMockMDnsM, times(1)).resolve(
-                eq(4), eq("a_name"), eq("a_type"), eq("local."), eq(0));
+        verify(mMockMDnsM).resolve(
+                4 /* id */, SERVICE_NAME, SERVICE_TYPE, "local." /* domain */, IFACE_IDX_ANY);
 
         // Client disconnects, stop the daemon after CLEANUP_DELAY_MS.
         deathRecipient.binderDied();
         verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
         // checks that request are cleaned
-        verify(mMockMDnsM, times(1)).stopOperation(eq(2));
-        verify(mMockMDnsM, times(1)).stopOperation(eq(3));
-        verify(mMockMDnsM, times(1)).stopOperation(eq(4));
+        verify(mMockMDnsM).stopOperation(2 /* id */);
+        verify(mMockMDnsM).stopOperation(3 /* id */);
+        verify(mMockMDnsM).stopOperation(4 /* id */);
     }
 
     @Test
     @EnableCompatChanges(NsdManager.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS)
     public void testCleanupDelayNoRequestActive() throws Exception {
-        NsdService service = makeService();
-        NsdManager client = connectClient(service);
+        final NsdManager client = connectClient(mService);
 
-        NsdServiceInfo request = new NsdServiceInfo("a_name", "a_type");
-        request.setPort(2201);
-        NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class);
+        final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
+        request.setPort(PORT);
+        final RegistrationListener listener1 = mock(RegistrationListener.class);
         client.registerService(request, PROTOCOL, listener1);
         waitForIdle();
-        verify(mMockMDnsM, times(1)).registerEventListener(any());
-        verify(mMockMDnsM, times(1)).startDaemon();
+        verify(mMockMDnsM).registerEventListener(any());
+        verify(mMockMDnsM).startDaemon();
         final INsdManagerCallback cb1 = getCallback();
         final IBinder.DeathRecipient deathRecipient = verifyLinkToDeath(cb1);
-        verify(mMockMDnsM, times(1)).registerService(
-                eq(2), eq("a_name"), eq("a_type"), eq(2201), any(), eq(0));
+        verify(mMockMDnsM).registerService(
+                eq(2), eq(SERVICE_NAME), eq(SERVICE_TYPE), eq(PORT), any(), eq(IFACE_IDX_ANY));
 
         client.unregisterService(listener1);
         waitForIdle();
-        verify(mMockMDnsM, times(1)).stopOperation(eq(2));
+        verify(mMockMDnsM).stopOperation(2 /* id */);
 
         verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
         reset(mMockMDnsM);
@@ -289,38 +291,37 @@
         verify(mMockMDnsM, never()).stopDaemon();
     }
 
-    @Test
-    public void testDiscoverOnTetheringDownstream() throws Exception {
-        NsdService service = makeService();
-        NsdManager client = connectClient(service);
-
-        final String serviceType = "a_type";
-        final String serviceName = "a_name";
-        final String domainName = "mytestdevice.local";
-        final int interfaceIdx = 123;
-        final NsdManager.DiscoveryListener discListener = mock(NsdManager.DiscoveryListener.class);
-        client.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discListener);
-        waitForIdle();
-
+    private IMDnsEventListener getEventListener() {
         final ArgumentCaptor<IMDnsEventListener> listenerCaptor =
                 ArgumentCaptor.forClass(IMDnsEventListener.class);
         verify(mMockMDnsM).registerEventListener(listenerCaptor.capture());
+        return listenerCaptor.getValue();
+    }
+
+    @Test
+    public void testDiscoverOnTetheringDownstream() throws Exception {
+        final NsdManager client = connectClient(mService);
+        final int interfaceIdx = 123;
+        final DiscoveryListener discListener = mock(DiscoveryListener.class);
+        client.discoverServices(SERVICE_TYPE, PROTOCOL, discListener);
+        waitForIdle();
+
+        final IMDnsEventListener eventListener = getEventListener();
         final ArgumentCaptor<Integer> discIdCaptor = ArgumentCaptor.forClass(Integer.class);
-        verify(mMockMDnsM).discover(discIdCaptor.capture(), eq(serviceType),
+        verify(mMockMDnsM).discover(discIdCaptor.capture(), eq(SERVICE_TYPE),
                 eq(0) /* interfaceIdx */);
         // NsdManager uses a separate HandlerThread to dispatch callbacks (on ServiceHandler), so
         // this needs to use a timeout
-        verify(discListener, timeout(TIMEOUT_MS)).onDiscoveryStarted(serviceType);
+        verify(discListener, timeout(TIMEOUT_MS)).onDiscoveryStarted(SERVICE_TYPE);
 
         final DiscoveryInfo discoveryInfo = new DiscoveryInfo(
                 discIdCaptor.getValue(),
                 IMDnsEventListener.SERVICE_FOUND,
-                serviceName,
-                serviceType,
-                domainName,
+                SERVICE_NAME,
+                SERVICE_TYPE,
+                DOMAIN_NAME,
                 interfaceIdx,
                 INetd.LOCAL_NET_ID); // LOCAL_NET_ID (99) used on tethering downstreams
-        final IMDnsEventListener eventListener = listenerCaptor.getValue();
         eventListener.onServiceDiscoveryStatus(discoveryInfo);
         waitForIdle();
 
@@ -328,31 +329,30 @@
                 ArgumentCaptor.forClass(NsdServiceInfo.class);
         verify(discListener, timeout(TIMEOUT_MS)).onServiceFound(discoveredInfoCaptor.capture());
         final NsdServiceInfo foundInfo = discoveredInfoCaptor.getValue();
-        assertEquals(serviceName, foundInfo.getServiceName());
-        assertEquals(serviceType, foundInfo.getServiceType());
+        assertEquals(SERVICE_NAME, foundInfo.getServiceName());
+        assertEquals(SERVICE_TYPE, foundInfo.getServiceType());
         assertNull(foundInfo.getHost());
         assertNull(foundInfo.getNetwork());
         assertEquals(interfaceIdx, foundInfo.getInterfaceIndex());
 
         // After discovering the service, verify resolving it
-        final NsdManager.ResolveListener resolveListener = mock(NsdManager.ResolveListener.class);
+        final ResolveListener resolveListener = mock(ResolveListener.class);
         client.resolveService(foundInfo, resolveListener);
         waitForIdle();
 
         final ArgumentCaptor<Integer> resolvIdCaptor = ArgumentCaptor.forClass(Integer.class);
-        verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(serviceName), eq(serviceType),
+        verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(SERVICE_NAME), eq(SERVICE_TYPE),
                 eq("local.") /* domain */, eq(interfaceIdx));
 
         final int servicePort = 10123;
-        final String serviceFullName = serviceName + "." + serviceType;
         final ResolutionInfo resolutionInfo = new ResolutionInfo(
                 resolvIdCaptor.getValue(),
                 IMDnsEventListener.SERVICE_RESOLVED,
                 null /* serviceName */,
                 null /* serviceType */,
                 null /* domain */,
-                serviceFullName,
-                domainName,
+                SERVICE_FULL_NAME,
+                DOMAIN_NAME,
                 servicePort,
                 new byte[0] /* txtRecord */,
                 interfaceIdx);
@@ -362,14 +362,14 @@
         waitForIdle();
 
         final ArgumentCaptor<Integer> getAddrIdCaptor = ArgumentCaptor.forClass(Integer.class);
-        verify(mMockMDnsM).getServiceAddress(getAddrIdCaptor.capture(), eq(domainName),
+        verify(mMockMDnsM).getServiceAddress(getAddrIdCaptor.capture(), eq(DOMAIN_NAME),
                 eq(interfaceIdx));
 
         final String serviceAddress = "192.0.2.123";
         final GetAddressInfo addressInfo = new GetAddressInfo(
                 getAddrIdCaptor.getValue(),
                 IMDnsEventListener.SERVICE_GET_ADDR_SUCCESS,
-                serviceFullName,
+                SERVICE_FULL_NAME,
                 serviceAddress,
                 interfaceIdx,
                 INetd.LOCAL_NET_ID);
@@ -380,14 +380,162 @@
                 ArgumentCaptor.forClass(NsdServiceInfo.class);
         verify(resolveListener, timeout(TIMEOUT_MS)).onServiceResolved(resInfoCaptor.capture());
         final NsdServiceInfo resolvedService = resInfoCaptor.getValue();
-        assertEquals(serviceName, resolvedService.getServiceName());
-        assertEquals("." + serviceType, resolvedService.getServiceType());
+        assertEquals(SERVICE_NAME, resolvedService.getServiceName());
+        assertEquals("." + SERVICE_TYPE, resolvedService.getServiceType());
         assertEquals(InetAddresses.parseNumericAddress(serviceAddress), resolvedService.getHost());
         assertEquals(servicePort, resolvedService.getPort());
         assertNull(resolvedService.getNetwork());
         assertEquals(interfaceIdx, resolvedService.getInterfaceIndex());
     }
 
+    @Test
+    public void testServiceRegistrationSuccessfulAndFailed() throws Exception {
+        final NsdManager client = connectClient(mService);
+        final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
+        request.setPort(PORT);
+        final RegistrationListener regListener = mock(RegistrationListener.class);
+        client.registerService(request, PROTOCOL, regListener);
+        waitForIdle();
+
+        final IMDnsEventListener eventListener = getEventListener();
+        final ArgumentCaptor<Integer> regIdCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(mMockMDnsM).registerService(regIdCaptor.capture(),
+                eq(SERVICE_NAME), eq(SERVICE_TYPE), eq(PORT), any(), eq(IFACE_IDX_ANY));
+
+        // Register service successfully.
+        final RegistrationInfo registrationInfo = new RegistrationInfo(
+                regIdCaptor.getValue(),
+                IMDnsEventListener.SERVICE_REGISTERED,
+                SERVICE_NAME,
+                SERVICE_TYPE,
+                PORT,
+                new byte[0] /* txtRecord */,
+                IFACE_IDX_ANY);
+        eventListener.onServiceRegistrationStatus(registrationInfo);
+
+        final ArgumentCaptor<NsdServiceInfo> registeredInfoCaptor =
+                ArgumentCaptor.forClass(NsdServiceInfo.class);
+        verify(regListener, timeout(TIMEOUT_MS))
+                .onServiceRegistered(registeredInfoCaptor.capture());
+        final NsdServiceInfo registeredInfo = registeredInfoCaptor.getValue();
+        assertEquals(SERVICE_NAME, registeredInfo.getServiceName());
+
+        // Fail to register service.
+        final RegistrationInfo registrationFailedInfo = new RegistrationInfo(
+                regIdCaptor.getValue(),
+                IMDnsEventListener.SERVICE_REGISTRATION_FAILED,
+                null /* serviceName */,
+                null /* registrationType */,
+                0 /* port */,
+                new byte[0] /* txtRecord */,
+                IFACE_IDX_ANY);
+        eventListener.onServiceRegistrationStatus(registrationFailedInfo);
+        verify(regListener, timeout(TIMEOUT_MS))
+                .onRegistrationFailed(any(), eq(FAILURE_INTERNAL_ERROR));
+    }
+
+    @Test
+    public void testServiceDiscoveryFailed() throws Exception {
+        final NsdManager client = connectClient(mService);
+        final DiscoveryListener discListener = mock(DiscoveryListener.class);
+        client.discoverServices(SERVICE_TYPE, PROTOCOL, discListener);
+        waitForIdle();
+
+        final IMDnsEventListener eventListener = getEventListener();
+        final ArgumentCaptor<Integer> discIdCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(mMockMDnsM).discover(discIdCaptor.capture(), eq(SERVICE_TYPE), eq(IFACE_IDX_ANY));
+        verify(discListener, timeout(TIMEOUT_MS)).onDiscoveryStarted(SERVICE_TYPE);
+
+        // Fail to discover service.
+        final DiscoveryInfo discoveryFailedInfo = new DiscoveryInfo(
+                discIdCaptor.getValue(),
+                IMDnsEventListener.SERVICE_DISCOVERY_FAILED,
+                null /* serviceName */,
+                null /* registrationType */,
+                null /* domainName */,
+                IFACE_IDX_ANY,
+                0 /* netId */);
+        eventListener.onServiceDiscoveryStatus(discoveryFailedInfo);
+        verify(discListener, timeout(TIMEOUT_MS))
+                .onStartDiscoveryFailed(SERVICE_TYPE, FAILURE_INTERNAL_ERROR);
+    }
+
+    @Test
+    public void testServiceResolutionFailed() throws Exception {
+        final NsdManager client = connectClient(mService);
+        final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
+        final ResolveListener resolveListener = mock(ResolveListener.class);
+        client.resolveService(request, resolveListener);
+        waitForIdle();
+
+        final IMDnsEventListener eventListener = getEventListener();
+        final ArgumentCaptor<Integer> resolvIdCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(SERVICE_NAME), eq(SERVICE_TYPE),
+                eq("local.") /* domain */, eq(IFACE_IDX_ANY));
+
+        // Fail to resolve service.
+        final ResolutionInfo resolutionFailedInfo = new ResolutionInfo(
+                resolvIdCaptor.getValue(),
+                IMDnsEventListener.SERVICE_RESOLUTION_FAILED,
+                null /* serviceName */,
+                null /* serviceType */,
+                null /* domain */,
+                null /* serviceFullName */,
+                null /* domainName */,
+                0 /* port */,
+                new byte[0] /* txtRecord */,
+                IFACE_IDX_ANY);
+        eventListener.onServiceResolutionStatus(resolutionFailedInfo);
+        verify(resolveListener, timeout(TIMEOUT_MS))
+                .onResolveFailed(any(), eq(FAILURE_INTERNAL_ERROR));
+    }
+
+    @Test
+    public void testGettingAddressFailed() throws Exception {
+        final NsdManager client = connectClient(mService);
+        final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
+        final ResolveListener resolveListener = mock(ResolveListener.class);
+        client.resolveService(request, resolveListener);
+        waitForIdle();
+
+        final IMDnsEventListener eventListener = getEventListener();
+        final ArgumentCaptor<Integer> resolvIdCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(SERVICE_NAME), eq(SERVICE_TYPE),
+                eq("local.") /* domain */, eq(IFACE_IDX_ANY));
+
+        // Resolve service successfully.
+        final ResolutionInfo resolutionInfo = new ResolutionInfo(
+                resolvIdCaptor.getValue(),
+                IMDnsEventListener.SERVICE_RESOLVED,
+                null /* serviceName */,
+                null /* serviceType */,
+                null /* domain */,
+                SERVICE_FULL_NAME,
+                DOMAIN_NAME,
+                PORT,
+                new byte[0] /* txtRecord */,
+                IFACE_IDX_ANY);
+        doReturn(true).when(mMockMDnsM).getServiceAddress(anyInt(), any(), anyInt());
+        eventListener.onServiceResolutionStatus(resolutionInfo);
+        waitForIdle();
+
+        final ArgumentCaptor<Integer> getAddrIdCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(mMockMDnsM).getServiceAddress(getAddrIdCaptor.capture(), eq(DOMAIN_NAME),
+                eq(IFACE_IDX_ANY));
+
+        // Fail to get service address.
+        final GetAddressInfo gettingAddrFailedInfo = new GetAddressInfo(
+                getAddrIdCaptor.getValue(),
+                IMDnsEventListener.SERVICE_GET_ADDR_FAILED,
+                null /* hostname */,
+                null /* address */,
+                IFACE_IDX_ANY,
+                0 /* netId */);
+        eventListener.onGettingServiceAddressStatus(gettingAddrFailedInfo);
+        verify(resolveListener, timeout(TIMEOUT_MS))
+                .onResolveFailed(any(), eq(FAILURE_INTERNAL_ERROR));
+    }
+
     private void waitForIdle() {
         HandlerUtils.waitForIdle(mHandler, TIMEOUT_MS);
     }
@@ -415,7 +563,10 @@
     }
 
     NsdManager connectClient(NsdService service) {
-        return new NsdManager(mContext, service);
+        final NsdManager nsdManager = new NsdManager(mContext, service);
+        // Wait for client registration done.
+        waitForIdle();
+        return nsdManager;
     }
 
     void verifyDelayMaybeStopDaemon(long cleanupDelayMs) throws Exception {
diff --git a/tests/unit/java/com/android/server/VpnManagerServiceTest.java b/tests/unit/java/com/android/server/VpnManagerServiceTest.java
new file mode 100644
index 0000000..c814cc5
--- /dev/null
+++ b/tests/unit/java/com/android/server/VpnManagerServiceTest.java
@@ -0,0 +1,244 @@
+/*
+ * 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;
+
+import static android.os.Build.VERSION_CODES.R;
+
+import static com.android.testutils.ContextUtils.mockService;
+import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import static com.android.testutils.MiscAsserts.assertThrows;
+
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.annotation.UserIdInt;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.INetd;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.INetworkManagementService;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.connectivity.Vpn;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.HandlerUtils;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@IgnoreUpTo(R) // VpnManagerService is not available before R
+@SmallTest
+public class VpnManagerServiceTest extends VpnTestBase {
+    @Rule
+    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
+    private static final int TIMEOUT_MS = 2_000;
+
+    @Mock Context mContext;
+    @Mock Context mSystemContext;
+    @Mock Context mUserAllContext;
+    private HandlerThread mHandlerThread;
+    @Mock private Vpn mVpn;
+    @Mock private INetworkManagementService mNms;
+    @Mock private ConnectivityManager mCm;
+    @Mock private UserManager mUserManager;
+    @Mock private INetd mNetd;
+    @Mock private PackageManager mPackageManager;
+
+    private VpnManagerServiceDependencies mDeps;
+    private VpnManagerService mService;
+    private BroadcastReceiver mUserPresentReceiver;
+    private BroadcastReceiver mIntentReceiver;
+    private final String mNotMyVpnPkg = "com.not.my.vpn";
+
+    class VpnManagerServiceDependencies extends VpnManagerService.Dependencies {
+        @Override
+        public HandlerThread makeHandlerThread() {
+            return mHandlerThread;
+        }
+
+        @Override
+        public INetworkManagementService getINetworkManagementService() {
+            return mNms;
+        }
+
+        @Override
+        public INetd getNetd() {
+            return mNetd;
+        }
+
+        @Override
+        public Vpn createVpn(Looper looper, Context context, INetworkManagementService nms,
+                INetd netd, @UserIdInt int userId) {
+            return mVpn;
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mHandlerThread = new HandlerThread("TestVpnManagerService");
+        mDeps = new VpnManagerServiceDependencies();
+        doReturn(mUserAllContext).when(mContext).createContextAsUser(UserHandle.ALL, 0);
+        doReturn(mSystemContext).when(mContext).createContextAsUser(UserHandle.SYSTEM, 0);
+        doReturn(mPackageManager).when(mContext).getPackageManager();
+        setMockedPackages(mPackageManager, sPackages);
+
+        mockService(mContext, ConnectivityManager.class, Context.CONNECTIVITY_SERVICE, mCm);
+        mockService(mContext, UserManager.class, Context.USER_SERVICE, mUserManager);
+        doReturn(SYSTEM_USER).when(mUserManager).getUserInfo(eq(SYSTEM_USER_ID));
+
+        mService = new VpnManagerService(mContext, mDeps);
+        mService.systemReady();
+
+        final ArgumentCaptor<BroadcastReceiver> intentReceiverCaptor =
+                ArgumentCaptor.forClass(BroadcastReceiver.class);
+        final ArgumentCaptor<BroadcastReceiver> userPresentReceiverCaptor =
+                ArgumentCaptor.forClass(BroadcastReceiver.class);
+        verify(mSystemContext).registerReceiver(
+                userPresentReceiverCaptor.capture(), any(), any(), any());
+        verify(mUserAllContext, times(2)).registerReceiver(
+                intentReceiverCaptor.capture(), any(), any(), any());
+        mUserPresentReceiver = userPresentReceiverCaptor.getValue();
+        mIntentReceiver = intentReceiverCaptor.getValue();
+
+        // Add user to create vpn in mVpn
+        onUserStarted(SYSTEM_USER_ID);
+        assertNotNull(mService.mVpns.get(SYSTEM_USER_ID));
+    }
+
+    @Test
+    public void testUpdateAppExclusionList() {
+        // Start vpn
+        mService.startVpnProfile(TEST_VPN_PKG);
+        verify(mVpn).startVpnProfile(eq(TEST_VPN_PKG));
+
+        // Remove package due to package replaced.
+        onPackageRemoved(PKGS[0], PKG_UIDS[0], true /* isReplacing */);
+        verify(mVpn, never()).refreshPlatformVpnAppExclusionList();
+
+        // Add package due to package replaced.
+        onPackageAdded(PKGS[0], PKG_UIDS[0], true /* isReplacing */);
+        verify(mVpn, never()).refreshPlatformVpnAppExclusionList();
+
+        // Remove package
+        onPackageRemoved(PKGS[0], PKG_UIDS[0], false /* isReplacing */);
+        verify(mVpn).refreshPlatformVpnAppExclusionList();
+
+        // Add the package back
+        onPackageAdded(PKGS[0], PKG_UIDS[0], false /* isReplacing */);
+        verify(mVpn, times(2)).refreshPlatformVpnAppExclusionList();
+    }
+
+    @Test
+    public void testStartVpnProfileFromDiffPackage() {
+        assertThrows(
+                SecurityException.class, () -> mService.startVpnProfile(mNotMyVpnPkg));
+    }
+
+    @Test
+    public void testStopVpnProfileFromDiffPackage() {
+        assertThrows(SecurityException.class, () -> mService.stopVpnProfile(mNotMyVpnPkg));
+    }
+
+    @Test
+    public void testGetProvisionedVpnProfileStateFromDiffPackage() {
+        assertThrows(SecurityException.class, () ->
+                mService.getProvisionedVpnProfileState(mNotMyVpnPkg));
+    }
+
+    @Test
+    public void testGetProvisionedVpnProfileState() {
+        mService.getProvisionedVpnProfileState(TEST_VPN_PKG);
+        verify(mVpn).getProvisionedVpnProfileState(TEST_VPN_PKG);
+    }
+
+    private Intent buildIntent(String action, String packageName, int userId, int uid,
+            boolean isReplacing) {
+        final Intent intent = new Intent(action);
+        intent.putExtra(Intent.EXTRA_USER_HANDLE, userId);
+        intent.putExtra(Intent.EXTRA_UID, uid);
+        intent.putExtra(Intent.EXTRA_REPLACING, isReplacing);
+        if (packageName != null) {
+            intent.setData(Uri.fromParts("package" /* scheme */, packageName, null /* fragment */));
+        }
+
+        return intent;
+    }
+
+    private void sendIntent(Intent intent) {
+        final Handler h = mHandlerThread.getThreadHandler();
+
+        // Send in handler thread.
+        h.post(() -> mIntentReceiver.onReceive(mContext, intent));
+        HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
+    }
+
+    private void onUserStarted(int userId) {
+        sendIntent(buildIntent(Intent.ACTION_USER_STARTED,
+                null /* packageName */, userId, -1 /* uid */, false /* isReplacing */));
+    }
+
+    private void onPackageAdded(String packageName, int userId, int uid, boolean isReplacing) {
+        sendIntent(buildIntent(Intent.ACTION_PACKAGE_ADDED, packageName, userId, uid, isReplacing));
+    }
+
+    private void onPackageAdded(String packageName, int uid, boolean isReplacing) {
+        onPackageAdded(packageName, UserHandle.USER_SYSTEM, uid, isReplacing);
+    }
+
+    private void onPackageRemoved(String packageName, int userId, int uid, boolean isReplacing) {
+        sendIntent(buildIntent(Intent.ACTION_PACKAGE_REMOVED, packageName, userId, uid,
+                isReplacing));
+    }
+
+    private void onPackageRemoved(String packageName, int uid, boolean isReplacing) {
+        onPackageRemoved(packageName, UserHandle.USER_SYSTEM, uid, isReplacing);
+    }
+
+    @Test
+    public void testReceiveIntentFromNonHandlerThread() {
+        assertThrows(IllegalStateException.class, () ->
+                mIntentReceiver.onReceive(mContext, buildIntent(Intent.ACTION_PACKAGE_REMOVED,
+                        PKGS[0], UserHandle.USER_SYSTEM, PKG_UIDS[0], true /* isReplacing */)));
+
+        assertThrows(IllegalStateException.class, () ->
+                mUserPresentReceiver.onReceive(mContext, new Intent(Intent.ACTION_USER_PRESENT)));
+    }
+}
diff --git a/tests/unit/java/com/android/server/VpnTestBase.java b/tests/unit/java/com/android/server/VpnTestBase.java
new file mode 100644
index 0000000..6113872
--- /dev/null
+++ b/tests/unit/java/com/android/server/VpnTestBase.java
@@ -0,0 +1,97 @@
+/*
+ * 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;
+
+import static android.content.pm.UserInfo.FLAG_ADMIN;
+import static android.content.pm.UserInfo.FLAG_MANAGED_PROFILE;
+import static android.content.pm.UserInfo.FLAG_PRIMARY;
+import static android.content.pm.UserInfo.FLAG_RESTRICTED;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+
+import android.content.pm.PackageManager;
+import android.content.pm.UserInfo;
+import android.os.Process;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/** Common variables or methods shared between VpnTest and VpnManagerServiceTest. */
+public class VpnTestBase {
+    protected static final String TEST_VPN_PKG = "com.testvpn.vpn";
+    /**
+     * Names and UIDs for some fake packages. Important points:
+     *  - UID is ordered increasing.
+     *  - One pair of packages have consecutive UIDs.
+     */
+    protected static final String[] PKGS = {"com.example", "org.example", "net.example", "web.vpn"};
+    protected static final int[] PKG_UIDS = {10066, 10077, 10078, 10400};
+    // Mock packages
+    protected static final Map<String, Integer> sPackages = new ArrayMap<>();
+    static {
+        for (int i = 0; i < PKGS.length; i++) {
+            sPackages.put(PKGS[i], PKG_UIDS[i]);
+        }
+        sPackages.put(TEST_VPN_PKG, Process.myUid());
+    }
+
+    // Mock users
+    protected static final int SYSTEM_USER_ID = 0;
+    protected static final UserInfo SYSTEM_USER = new UserInfo(0, "system", UserInfo.FLAG_PRIMARY);
+    protected static final UserInfo PRIMARY_USER = new UserInfo(27, "Primary",
+            FLAG_ADMIN | FLAG_PRIMARY);
+    protected static final UserInfo SECONDARY_USER = new UserInfo(15, "Secondary", FLAG_ADMIN);
+    protected static final UserInfo RESTRICTED_PROFILE_A = new UserInfo(40, "RestrictedA",
+            FLAG_RESTRICTED);
+    protected static final UserInfo RESTRICTED_PROFILE_B = new UserInfo(42, "RestrictedB",
+            FLAG_RESTRICTED);
+    protected static final UserInfo MANAGED_PROFILE_A = new UserInfo(45, "ManagedA",
+            FLAG_MANAGED_PROFILE);
+    static {
+        RESTRICTED_PROFILE_A.restrictedProfileParentId = PRIMARY_USER.id;
+        RESTRICTED_PROFILE_B.restrictedProfileParentId = SECONDARY_USER.id;
+        MANAGED_PROFILE_A.profileGroupId = PRIMARY_USER.id;
+    }
+
+    // Populate a fake packageName-to-UID mapping.
+    protected void setMockedPackages(PackageManager mockPm, final Map<String, Integer> packages) {
+        try {
+            doAnswer(invocation -> {
+                final String appName = (String) invocation.getArguments()[0];
+                final int userId = (int) invocation.getArguments()[1];
+
+                final Integer appId = packages.get(appName);
+                if (appId == null) {
+                    throw new PackageManager.NameNotFoundException(appName);
+                }
+
+                return UserHandle.getUid(userId, appId);
+            }).when(mockPm).getPackageUidAsUser(anyString(), anyInt());
+        } catch (Exception e) {
+        }
+    }
+
+    protected List<Integer> toList(int[] arr) {
+        return Arrays.stream(arr).boxed().collect(Collectors.toList());
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
index 8dfe924..feee293 100644
--- a/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
@@ -525,16 +525,19 @@
         coordinator.dump(ipw);
 
         final String[] dumpStrings = stringWriter.toString().split("\n");
-        assertEquals(5, dumpStrings.length);
-        assertEquals("Forwarding rules:", dumpStrings[0].trim());
+        assertEquals(6, dumpStrings.length);
+        assertEquals("CLAT tracker: iface: test0 (1000), v4iface: v4-test0 (1001), "
+                + "v4: /192.0.0.46, v6: /2001:db8:0:b11::464, pfx96: /64:ff9b::, "
+                + "pid: 10483, cookie: 27149", dumpStrings[0].trim());
+        assertEquals("Forwarding rules:", dumpStrings[1].trim());
         assertEquals("BPF ingress map: iif nat64Prefix v6Addr -> v4Addr oif",
-                dumpStrings[1].trim());
-        assertEquals("1000 /64:ff9b::/96 /2001:db8:0:b11::464 -> /192.0.0.46 1001",
                 dumpStrings[2].trim());
-        assertEquals("BPF egress map: iif v4Addr -> v6Addr nat64Prefix oif",
+        assertEquals("1000 /64:ff9b::/96 /2001:db8:0:b11::464 -> /192.0.0.46 1001",
                 dumpStrings[3].trim());
-        assertEquals("1001 /192.0.0.46 -> /2001:db8:0:b11::464 /64:ff9b::/96 1000 ether",
+        assertEquals("BPF egress map: iif v4Addr -> v6Addr nat64Prefix oif",
                 dumpStrings[4].trim());
+        assertEquals("1001 /192.0.0.46 -> /2001:db8:0:b11::464 /64:ff9b::/96 1000 ether",
+                dumpStrings[5].trim());
     }
 
     @Test
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index 8f1d3b8..6f25d1b 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -20,10 +20,6 @@
 import static android.Manifest.permission.CONTROL_VPN;
 import static android.content.pm.PackageManager.PERMISSION_DENIED;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
-import static android.content.pm.UserInfo.FLAG_ADMIN;
-import static android.content.pm.UserInfo.FLAG_MANAGED_PROFILE;
-import static android.content.pm.UserInfo.FLAG_PRIMARY;
-import static android.content.pm.UserInfo.FLAG_RESTRICTED;
 import static android.net.ConnectivityManager.NetworkCallback;
 import static android.net.INetd.IF_STATE_DOWN;
 import static android.net.INetd.IF_STATE_UP;
@@ -34,6 +30,7 @@
 import static android.os.UserHandle.PER_USER_RANGE;
 
 import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
+import static com.android.testutils.Cleanup.testAndCleanup;
 import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import static com.android.testutils.MiscAsserts.assertThrows;
 
@@ -96,10 +93,8 @@
 import android.net.LocalSocket;
 import android.net.Network;
 import android.net.NetworkAgent;
-import android.net.NetworkAgentConfig;
 import android.net.NetworkCapabilities;
 import android.net.NetworkInfo.DetailedState;
-import android.net.NetworkProvider;
 import android.net.RouteInfo;
 import android.net.UidRangeParcel;
 import android.net.VpnManager;
@@ -121,7 +116,6 @@
 import android.os.Bundle;
 import android.os.ConditionVariable;
 import android.os.INetworkManagementService;
-import android.os.Looper;
 import android.os.ParcelFileDescriptor;
 import android.os.PowerWhitelistManager;
 import android.os.Process;
@@ -145,6 +139,7 @@
 import com.android.modules.utils.build.SdkLevel;
 import com.android.server.DeviceIdleInternal;
 import com.android.server.IpSecService;
+import com.android.server.VpnTestBase;
 import com.android.server.vcn.util.PersistableBundleUtils;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
@@ -177,6 +172,8 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
@@ -191,28 +188,15 @@
  */
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@IgnoreUpTo(VERSION_CODES.S_V2)
-public class VpnTest {
+@IgnoreUpTo(S_V2)
+public class VpnTest extends VpnTestBase {
     private static final String TAG = "VpnTest";
 
     @Rule
     public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
 
-    // Mock users
-    static final UserInfo primaryUser = new UserInfo(27, "Primary", FLAG_ADMIN | FLAG_PRIMARY);
-    static final UserInfo secondaryUser = new UserInfo(15, "Secondary", FLAG_ADMIN);
-    static final UserInfo restrictedProfileA = new UserInfo(40, "RestrictedA", FLAG_RESTRICTED);
-    static final UserInfo restrictedProfileB = new UserInfo(42, "RestrictedB", FLAG_RESTRICTED);
-    static final UserInfo managedProfileA = new UserInfo(45, "ManagedA", FLAG_MANAGED_PROFILE);
-    static {
-        restrictedProfileA.restrictedProfileParentId = primaryUser.id;
-        restrictedProfileB.restrictedProfileParentId = secondaryUser.id;
-        managedProfileA.profileGroupId = primaryUser.id;
-    }
-
     static final Network EGRESS_NETWORK = new Network(101);
     static final String EGRESS_IFACE = "wlan0";
-    static final String TEST_VPN_PKG = "com.testvpn.vpn";
     private static final String TEST_VPN_CLIENT = "2.4.6.8";
     private static final String TEST_VPN_SERVER = "1.2.3.4";
     private static final String TEST_VPN_IDENTITY = "identity";
@@ -248,24 +232,9 @@
     private static final int TEST_TUNNEL_RESOURCE_ID = 0x2345;
     private static final long TEST_TIMEOUT_MS = 500L;
     private static final String PRIMARY_USER_APP_EXCLUDE_KEY =
-            "VPN_APP_EXCLUDED_27_com.testvpn.vpn";
-    /**
-     * Names and UIDs for some fake packages. Important points:
-     *  - UID is ordered increasing.
-     *  - One pair of packages have consecutive UIDs.
-     */
-    static final String[] PKGS = {"com.example", "org.example", "net.example", "web.vpn"};
+            "VPNAPPEXCLUDED_27_com.testvpn.vpn";
     static final String PKGS_BYTES = getPackageByteString(List.of(PKGS));
-    static final int[] PKG_UIDS = {10066, 10077, 10078, 10400};
-
-    // Mock packages
-    static final Map<String, Integer> mPackages = new ArrayMap<>();
-    static {
-        for (int i = 0; i < PKGS.length; i++) {
-            mPackages.put(PKGS[i], PKG_UIDS[i]);
-        }
-    }
-    private static final Range<Integer> PRI_USER_RANGE = uidRangeForUser(primaryUser.id);
+    private static final Range<Integer> PRIMARY_USER_RANGE = uidRangeForUser(PRIMARY_USER.id);
 
     @Mock(answer = Answers.RETURNS_DEEP_STUBS) private Context mContext;
     @Mock private UserManager mUserManager;
@@ -307,7 +276,7 @@
         mTestDeps = spy(new TestDeps());
 
         when(mContext.getPackageManager()).thenReturn(mPackageManager);
-        setMockedPackages(mPackages);
+        setMockedPackages(sPackages);
 
         when(mContext.getPackageName()).thenReturn(TEST_VPN_PKG);
         when(mContext.getOpPackageName()).thenReturn(TEST_VPN_PKG);
@@ -412,50 +381,51 @@
 
     @Test
     public void testRestrictedProfilesAreAddedToVpn() {
-        setMockedUsers(primaryUser, secondaryUser, restrictedProfileA, restrictedProfileB);
+        setMockedUsers(PRIMARY_USER, SECONDARY_USER, RESTRICTED_PROFILE_A, RESTRICTED_PROFILE_B);
 
-        final Vpn vpn = createVpn(primaryUser.id);
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
 
         // Assume the user can have restricted profiles.
         doReturn(true).when(mUserManager).canHaveRestrictedProfile();
         final Set<Range<Integer>> ranges =
-                vpn.createUserAndRestrictedProfilesRanges(primaryUser.id, null, null);
+                vpn.createUserAndRestrictedProfilesRanges(PRIMARY_USER.id, null, null);
 
-        assertEquals(rangeSet(PRI_USER_RANGE, uidRangeForUser(restrictedProfileA.id)), ranges);
+        assertEquals(rangeSet(PRIMARY_USER_RANGE, uidRangeForUser(RESTRICTED_PROFILE_A.id)),
+                 ranges);
     }
 
     @Test
     public void testManagedProfilesAreNotAddedToVpn() {
-        setMockedUsers(primaryUser, managedProfileA);
+        setMockedUsers(PRIMARY_USER, MANAGED_PROFILE_A);
 
-        final Vpn vpn = createVpn(primaryUser.id);
-        final Set<Range<Integer>> ranges = vpn.createUserAndRestrictedProfilesRanges(primaryUser.id,
-                null, null);
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
+        final Set<Range<Integer>> ranges = vpn.createUserAndRestrictedProfilesRanges(
+                PRIMARY_USER.id, null, null);
 
-        assertEquals(rangeSet(PRI_USER_RANGE), ranges);
+        assertEquals(rangeSet(PRIMARY_USER_RANGE), ranges);
     }
 
     @Test
     public void testAddUserToVpnOnlyAddsOneUser() {
-        setMockedUsers(primaryUser, restrictedProfileA, managedProfileA);
+        setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A, MANAGED_PROFILE_A);
 
-        final Vpn vpn = createVpn(primaryUser.id);
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
         final Set<Range<Integer>> ranges = new ArraySet<>();
-        vpn.addUserToRanges(ranges, primaryUser.id, null, null);
+        vpn.addUserToRanges(ranges, PRIMARY_USER.id, null, null);
 
-        assertEquals(rangeSet(PRI_USER_RANGE), ranges);
+        assertEquals(rangeSet(PRIMARY_USER_RANGE), ranges);
     }
 
     @Test
     public void testUidAllowAndDenylist() throws Exception {
-        final Vpn vpn = createVpn(primaryUser.id);
-        final Range<Integer> user = PRI_USER_RANGE;
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
+        final Range<Integer> user = PRIMARY_USER_RANGE;
         final int userStart = user.getLower();
         final int userStop = user.getUpper();
         final String[] packages = {PKGS[0], PKGS[1], PKGS[2]};
 
         // Allowed list
-        final Set<Range<Integer>> allow = vpn.createUserAndRestrictedProfilesRanges(primaryUser.id,
+        final Set<Range<Integer>> allow = vpn.createUserAndRestrictedProfilesRanges(PRIMARY_USER.id,
                 Arrays.asList(packages), null /* disallowedApplications */);
         assertEquals(rangeSet(
                 uidRange(userStart + PKG_UIDS[0], userStart + PKG_UIDS[0]),
@@ -468,7 +438,7 @@
 
         // Denied list
         final Set<Range<Integer>> disallow =
-                vpn.createUserAndRestrictedProfilesRanges(primaryUser.id,
+                vpn.createUserAndRestrictedProfilesRanges(PRIMARY_USER.id,
                         null /* allowedApplications */, Arrays.asList(packages));
         assertEquals(rangeSet(
                 uidRange(userStart, userStart + PKG_UIDS[0] - 1),
@@ -490,7 +460,7 @@
 
     @Test
     public void testGetAlwaysAndOnGetLockDown() throws Exception {
-        final Vpn vpn = createVpn(primaryUser.id);
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
 
         // Default state.
         assertFalse(vpn.getAlwaysOn());
@@ -514,8 +484,8 @@
 
     @Test
     public void testLockdownChangingPackage() throws Exception {
-        final Vpn vpn = createVpn(primaryUser.id);
-        final Range<Integer> user = PRI_USER_RANGE;
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
+        final Range<Integer> user = PRIMARY_USER_RANGE;
         final int userStart = user.getLower();
         final int userStop = user.getUpper();
         // Set always-on without lockdown.
@@ -548,8 +518,8 @@
 
     @Test
     public void testLockdownAllowlist() throws Exception {
-        final Vpn vpn = createVpn(primaryUser.id);
-        final Range<Integer> user = PRI_USER_RANGE;
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
+        final Range<Integer> user = PRIMARY_USER_RANGE;
         final int userStart = user.getLower();
         final int userStop = user.getUpper();
         // Set always-on with lockdown and allow app PKGS[2] from lockdown.
@@ -659,9 +629,9 @@
 
     @Test
     public void testLockdownRuleRepeatability() throws Exception {
-        final Vpn vpn = createVpn(primaryUser.id);
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
         final UidRangeParcel[] primaryUserRangeParcel = new UidRangeParcel[] {
-                new UidRangeParcel(PRI_USER_RANGE.getLower(), PRI_USER_RANGE.getUpper())};
+                new UidRangeParcel(PRIMARY_USER_RANGE.getLower(), PRIMARY_USER_RANGE.getUpper())};
         // Given legacy lockdown is already enabled,
         vpn.setLockdown(true);
         verify(mConnectivityManager, times(1)).setRequireVpnForUids(true,
@@ -692,9 +662,9 @@
     @Test
     public void testLockdownRuleReversibility() throws Exception {
         doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
-        final Vpn vpn = createVpn(primaryUser.id);
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
         final UidRangeParcel[] entireUser = {
-            new UidRangeParcel(PRI_USER_RANGE.getLower(), PRI_USER_RANGE.getUpper())
+            new UidRangeParcel(PRIMARY_USER_RANGE.getLower(), PRIMARY_USER_RANGE.getUpper())
         };
         final UidRangeParcel[] exceptPkg0 = {
             new UidRangeParcel(entireUser[0].start, entireUser[0].start + PKG_UIDS[0] - 1),
@@ -744,17 +714,17 @@
 
     @Test
     public void testIsAlwaysOnPackageSupported() throws Exception {
-        final Vpn vpn = createVpn(primaryUser.id);
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
 
         ApplicationInfo appInfo = new ApplicationInfo();
-        when(mPackageManager.getApplicationInfoAsUser(eq(PKGS[0]), anyInt(), eq(primaryUser.id)))
+        when(mPackageManager.getApplicationInfoAsUser(eq(PKGS[0]), anyInt(), eq(PRIMARY_USER.id)))
                 .thenReturn(appInfo);
 
         ServiceInfo svcInfo = new ServiceInfo();
         ResolveInfo resInfo = new ResolveInfo();
         resInfo.serviceInfo = svcInfo;
         when(mPackageManager.queryIntentServicesAsUser(any(), eq(PackageManager.GET_META_DATA),
-                eq(primaryUser.id)))
+                eq(PRIMARY_USER.id)))
                 .thenReturn(Collections.singletonList(resInfo));
 
         // null package name should return false
@@ -778,9 +748,9 @@
 
     @Test
     public void testNotificationShownForAlwaysOnApp() throws Exception {
-        final UserHandle userHandle = UserHandle.of(primaryUser.id);
-        final Vpn vpn = createVpn(primaryUser.id);
-        setMockedUsers(primaryUser);
+        final UserHandle userHandle = UserHandle.of(PRIMARY_USER.id);
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
+        setMockedUsers(PRIMARY_USER);
 
         final InOrder order = inOrder(mNotificationManager);
 
@@ -813,15 +783,15 @@
      */
     @Test
     public void testGetProfileNameForPackage() throws Exception {
-        final Vpn vpn = createVpn(primaryUser.id);
-        setMockedUsers(primaryUser);
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
+        setMockedUsers(PRIMARY_USER);
 
-        final String expected = Credentials.PLATFORM_VPN + primaryUser.id + "_" + TEST_VPN_PKG;
+        final String expected = Credentials.PLATFORM_VPN + PRIMARY_USER.id + "_" + TEST_VPN_PKG;
         assertEquals(expected, vpn.getProfileNameForPackage(TEST_VPN_PKG));
     }
 
     private Vpn createVpnAndSetupUidChecks(String... grantedOps) throws Exception {
-        return createVpnAndSetupUidChecks(primaryUser, grantedOps);
+        return createVpnAndSetupUidChecks(PRIMARY_USER, grantedOps);
     }
 
     private Vpn createVpnAndSetupUidChecks(UserInfo user, String... grantedOps) throws Exception {
@@ -878,14 +848,11 @@
 
         vpn.startVpnProfile(TEST_VPN_PKG);
         verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
-        vpn.mNetworkAgent = new NetworkAgent(mContext, Looper.getMainLooper(), TAG,
-                new NetworkCapabilities.Builder().build(), new LinkProperties(), 10 /* score */,
-                new NetworkAgentConfig.Builder().build(),
-                new NetworkProvider(mContext, Looper.getMainLooper(), TAG)) {};
+        vpn.mNetworkAgent = mMockNetworkAgent;
         return vpn;
     }
 
-    @Test @IgnoreUpTo(S_V2)
+    @Test
     public void testSetAndGetAppExclusionList() throws Exception {
         final Vpn vpn = prepareVpnForVerifyAppExclusionList();
         verify(mVpnProfileStore, never()).put(eq(PRIMARY_USER_APP_EXCLUDE_KEY), any());
@@ -894,16 +861,90 @@
                 .put(eq(PRIMARY_USER_APP_EXCLUDE_KEY),
                      eq(HexDump.hexStringToByteArray(PKGS_BYTES)));
         assertEquals(vpn.createUserAndRestrictedProfilesRanges(
-                primaryUser.id, null, Arrays.asList(PKGS)),
+                PRIMARY_USER.id, null, Arrays.asList(PKGS)),
                 vpn.mNetworkCapabilities.getUids());
         assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
     }
 
-    @Test @IgnoreUpTo(S_V2)
+    @Test
+    public void testRefreshPlatformVpnAppExclusionList_updatesExcludedUids() throws Exception {
+        final Vpn vpn = prepareVpnForVerifyAppExclusionList();
+        vpn.setAppExclusionList(TEST_VPN_PKG, Arrays.asList(PKGS));
+        verify(mMockNetworkAgent).sendNetworkCapabilities(any());
+        assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
+
+        reset(mMockNetworkAgent);
+
+        // Remove one of the package
+        List<Integer> newExcludedUids = toList(PKG_UIDS);
+        newExcludedUids.remove((Integer) PKG_UIDS[0]);
+        sPackages.remove(PKGS[0]);
+        vpn.refreshPlatformVpnAppExclusionList();
+
+        // List in keystore is not changed, but UID for the removed packages is no longer exempted.
+        assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
+        assertEquals(makeVpnUidRange(PRIMARY_USER.id, newExcludedUids),
+                vpn.mNetworkCapabilities.getUids());
+        ArgumentCaptor<NetworkCapabilities> ncCaptor =
+                ArgumentCaptor.forClass(NetworkCapabilities.class);
+        verify(mMockNetworkAgent).sendNetworkCapabilities(ncCaptor.capture());
+        assertEquals(makeVpnUidRange(PRIMARY_USER.id, newExcludedUids),
+                ncCaptor.getValue().getUids());
+
+        reset(mMockNetworkAgent);
+
+        // Add the package back
+        newExcludedUids.add(PKG_UIDS[0]);
+        sPackages.put(PKGS[0], PKG_UIDS[0]);
+        vpn.refreshPlatformVpnAppExclusionList();
+
+        // List in keystore is not changed and the uid list should be updated in the net cap.
+        assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
+        assertEquals(makeVpnUidRange(PRIMARY_USER.id, newExcludedUids),
+                vpn.mNetworkCapabilities.getUids());
+        verify(mMockNetworkAgent).sendNetworkCapabilities(ncCaptor.capture());
+        assertEquals(makeVpnUidRange(PRIMARY_USER.id, newExcludedUids),
+                ncCaptor.getValue().getUids());
+    }
+
+    private Set<Range<Integer>> makeVpnUidRange(int userId, List<Integer> excludedList) {
+        final SortedSet<Integer> list = new TreeSet<>();
+
+        final int userBase = userId * UserHandle.PER_USER_RANGE;
+        for (int uid : excludedList) {
+            final int applicationUid = UserHandle.getUid(userId, uid);
+            list.add(applicationUid);
+            list.add(Process.toSdkSandboxUid(applicationUid)); // Add Sdk Sandbox UID
+        }
+
+        final int minUid = userBase;
+        final int maxUid = userBase + UserHandle.PER_USER_RANGE - 1;
+        final Set<Range<Integer>> ranges = new ArraySet<>();
+
+        // Iterate the list to create the ranges between each uid.
+        int start = minUid;
+        for (int uid : list) {
+            if (uid == start) {
+                start++;
+            } else {
+                ranges.add(new Range<>(start, uid - 1));
+                start = uid + 1;
+            }
+        }
+
+        // Create the range between last uid and max uid.
+        if (start <= maxUid) {
+            ranges.add(new Range<>(start, maxUid));
+        }
+
+        return ranges;
+    }
+
+    @Test
     public void testSetAndGetAppExclusionListRestrictedUser() throws Exception {
         final Vpn vpn = prepareVpnForVerifyAppExclusionList();
         // Mock it to restricted profile
-        when(mUserManager.getUserInfo(anyInt())).thenReturn(restrictedProfileA);
+        when(mUserManager.getUserInfo(anyInt())).thenReturn(RESTRICTED_PROFILE_A);
         // Restricted users cannot configure VPNs
         assertThrows(SecurityException.class,
                 () -> vpn.setAppExclusionList(TEST_VPN_PKG, new ArrayList<>()));
@@ -928,6 +969,31 @@
                 AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN, AppOpsManager.OPSTR_ACTIVATE_VPN);
     }
 
+    private void setAppOpsPermission() {
+        doAnswer(invocation -> {
+            when(mAppOps.noteOpNoThrow(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN,
+                    Process.myUid(), TEST_VPN_PKG,
+                    null /* attributionTag */, null /* message */))
+                    .thenReturn(AppOpsManager.MODE_ALLOWED);
+            return null;
+        }).when(mAppOps).setMode(
+                eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
+                eq(Process.myUid()),
+                eq(TEST_VPN_PKG),
+                eq(AppOpsManager.MODE_ALLOWED));
+    }
+
+    @Test
+    public void testProvisionVpnProfileNotPreconsented_withControlVpnPermission() throws Exception {
+        setAppOpsPermission();
+        doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
+        final Vpn vpn = createVpnAndSetupUidChecks();
+
+        // ACTIVATE_PLATFORM_VPN will be granted if VPN app has CONTROL_VPN permission.
+        checkProvisionVpnProfile(vpn, true /* expectedResult */,
+                AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+    }
+
     @Test
     public void testProvisionVpnProfileVpnServicePreconsented() throws Exception {
         final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_VPN);
@@ -953,7 +1019,7 @@
     public void testProvisionVpnProfileRestrictedUser() throws Exception {
         final Vpn vpn =
                 createVpnAndSetupUidChecks(
-                        restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+                        RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
 
         try {
             vpn.provisionVpnProfile(TEST_VPN_PKG, mVpnProfile);
@@ -976,7 +1042,7 @@
     public void testDeleteVpnProfileRestrictedUser() throws Exception {
         final Vpn vpn =
                 createVpnAndSetupUidChecks(
-                        restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+                        RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
 
         try {
             vpn.deleteVpnProfile(TEST_VPN_PKG);
@@ -1099,7 +1165,7 @@
     public void testStartVpnProfileRestrictedUser() throws Exception {
         final Vpn vpn =
                 createVpnAndSetupUidChecks(
-                        restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+                        RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
 
         try {
             vpn.startVpnProfile(TEST_VPN_PKG);
@@ -1112,7 +1178,7 @@
     public void testStopVpnProfileRestrictedUser() throws Exception {
         final Vpn vpn =
                 createVpnAndSetupUidChecks(
-                        restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+                        RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
 
         try {
             vpn.stopVpnProfile(TEST_VPN_PKG);
@@ -1183,7 +1249,7 @@
     private void verifyVpnManagerEvent(String sessionKey, String category, int errorClass,
             int errorCode, VpnProfileState... profileState) {
         final Context userContext =
-                mContext.createContextAsUser(UserHandle.of(primaryUser.id), 0 /* flags */);
+                mContext.createContextAsUser(UserHandle.of(PRIMARY_USER.id), 0 /* flags */);
         final ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
 
         final int verifyTimes = (profileState == null) ? 1 : profileState.length;
@@ -1250,7 +1316,7 @@
         assumeTrue(SdkLevel.isAtLeastT());
         // Calling setAlwaysOnPackage() needs to hold CONTROL_VPN.
         doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
-        final Vpn vpn = createVpn(primaryUser.id);
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
         // Enable VPN always-on for PKGS[1].
         assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false /* lockdown */,
                 null /* lockdownAllowlist */));
@@ -1319,6 +1385,31 @@
     }
 
     @Test
+    public void testReconnectVpnManagerVpnWithAlwaysOnEnabled() throws Exception {
+        final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
+                .thenReturn(mVpnProfile.encode());
+        vpn.startVpnProfile(TEST_VPN_PKG);
+        verifyPlatformVpnIsActivated(TEST_VPN_PKG);
+
+        // Enable VPN always-on for TEST_VPN_PKG.
+        assertTrue(vpn.setAlwaysOnPackage(TEST_VPN_PKG, false /* lockdown */,
+                null /* lockdownAllowlist */));
+
+        // Reset to verify next startVpnProfile.
+        reset(mAppOps);
+
+        vpn.stopVpnProfile(TEST_VPN_PKG);
+
+        // Reconnect the vpn with different package will cause exception.
+        assertThrows(SecurityException.class, () -> vpn.startVpnProfile(PKGS[0]));
+
+        // Reconnect the vpn again with the vpn always on package w/o exception.
+        vpn.startVpnProfile(TEST_VPN_PKG);
+        verifyPlatformVpnIsActivated(TEST_VPN_PKG);
+    }
+
+    @Test
     public void testSetPackageAuthorizationVpnService() throws Exception {
         final Vpn vpn = createVpnAndSetupUidChecks();
 
@@ -1512,7 +1603,7 @@
     public void testStartPlatformVpnIllegalArgumentExceptionInSetup() throws Exception {
         when(mIkev2SessionCreator.createIkeSession(any(), any(), any(), any(), any(), any()))
                 .thenThrow(new IllegalArgumentException());
-        final Vpn vpn = startLegacyVpn(createVpn(primaryUser.id), mVpnProfile);
+        final Vpn vpn = startLegacyVpn(createVpn(PRIMARY_USER.id), mVpnProfile);
         final NetworkCallback cb = triggerOnAvailableAndGetCallback();
 
         verifyInterfaceSetCfgWithFlags(IF_STATE_UP);
@@ -1523,6 +1614,30 @@
         assertEquals(LegacyVpnInfo.STATE_FAILED, vpn.getLegacyVpnInfo().state);
     }
 
+    @Test
+    public void testVpnManagerEventWillNotBeSentToSettingsVpn() throws Exception {
+        startLegacyVpn(createVpn(PRIMARY_USER.id), mVpnProfile);
+        triggerOnAvailableAndGetCallback();
+
+        verifyInterfaceSetCfgWithFlags(IF_STATE_UP);
+
+        final IkeNonProtocolException exception = mock(IkeNonProtocolException.class);
+        final IkeTimeoutException ikeTimeoutException =
+                new IkeTimeoutException("IkeTimeoutException");
+        when(exception.getCause()).thenReturn(ikeTimeoutException);
+
+        final ArgumentCaptor<IkeSessionCallback> captor =
+                ArgumentCaptor.forClass(IkeSessionCallback.class);
+        verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS))
+                .createIkeSession(any(), any(), any(), any(), captor.capture(), any());
+        final IkeSessionCallback ikeCb = captor.getValue();
+        ikeCb.onClosedWithException(exception);
+
+        final Context userContext =
+                mContext.createContextAsUser(UserHandle.of(PRIMARY_USER.id), 0 /* flags */);
+        verify(userContext, never()).startService(any());
+    }
+
     private void setAndVerifyAlwaysOnPackage(Vpn vpn, int uid, boolean lockdownEnabled) {
         assertTrue(vpn.setAlwaysOnPackage(TEST_VPN_PKG, lockdownEnabled, null));
 
@@ -1532,18 +1647,18 @@
                 eq(AppOpsManager.MODE_ALLOWED));
 
         verify(mSystemServices).settingsSecurePutStringForUser(
-                eq(Settings.Secure.ALWAYS_ON_VPN_APP), eq(TEST_VPN_PKG), eq(primaryUser.id));
+                eq(Settings.Secure.ALWAYS_ON_VPN_APP), eq(TEST_VPN_PKG), eq(PRIMARY_USER.id));
         verify(mSystemServices).settingsSecurePutIntForUser(
                 eq(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN), eq(lockdownEnabled ? 1 : 0),
-                eq(primaryUser.id));
+                eq(PRIMARY_USER.id));
         verify(mSystemServices).settingsSecurePutStringForUser(
-                eq(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN_WHITELIST), eq(""), eq(primaryUser.id));
+                eq(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN_WHITELIST), eq(""), eq(PRIMARY_USER.id));
     }
 
     @Test
     public void testSetAndStartAlwaysOnVpn() throws Exception {
-        final Vpn vpn = createVpn(primaryUser.id);
-        setMockedUsers(primaryUser);
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
+        setMockedUsers(PRIMARY_USER);
 
         // UID checks must return a different UID; otherwise it'll be treated as already prepared.
         final int uid = Process.myUid() + 1;
@@ -1560,7 +1675,7 @@
     }
 
     private Vpn startLegacyVpn(final Vpn vpn, final VpnProfile vpnProfile) throws Exception {
-        setMockedUsers(primaryUser);
+        setMockedUsers(PRIMARY_USER);
 
         // Dummy egress interface
         final LinkProperties lp = new LinkProperties();
@@ -1876,11 +1991,10 @@
         doReturn(new Network(102)).when(mConnectivityManager).registerNetworkAgent(any(), any(),
                 any(), any(), any(), any(), anyInt());
 
-        final Vpn vpn = startLegacyVpn(createVpn(primaryUser.id), profile);
+        final Vpn vpn = startLegacyVpn(createVpn(PRIMARY_USER.id), profile);
         final TestDeps deps = (TestDeps) vpn.mDeps;
 
-        // TODO: use import when this is merged in all branches and there's no merge conflict
-        com.android.testutils.Cleanup.testAndCleanup(() -> {
+        testAndCleanup(() -> {
             final String[] mtpdArgs = deps.mtpdArgs.get(10, TimeUnit.SECONDS);
             final String[] argsPrefix = new String[]{
                     EGRESS_IFACE, "pptp", profile.server, "1723", "name", profile.username,
@@ -1928,7 +2042,7 @@
                     legacyRunnerReady.open();
                     return new Network(102);
                 });
-        final Vpn vpn = startLegacyVpn(createVpn(primaryUser.id), profile);
+        final Vpn vpn = startLegacyVpn(createVpn(PRIMARY_USER.id), profile);
         final TestDeps deps = (TestDeps) vpn.mDeps;
         try {
             // udppsk and 1701 are the values for TYPE_L2TP_IPSEC_PSK
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
index 2178b33..b42058f 100644
--- a/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
+++ b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
@@ -320,14 +320,21 @@
     public void testUpdateInterfaceLinkStateForProvisionedInterface() throws Exception {
         initEthernetNetworkFactory();
         createAndVerifyProvisionedInterface(TEST_IFACE);
-        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+        final TestNetworkManagementListener listenerDown = new TestNetworkManagementListener();
+        final TestNetworkManagementListener listenerUp = new TestNetworkManagementListener();
 
-        final boolean ret =
-                mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */, listener);
+        final boolean retDown =
+                mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */, listenerDown);
 
-        assertTrue(ret);
+        assertTrue(retDown);
         verifyStop();
-        assertEquals(listener.expectOnResult(), TEST_IFACE);
+        assertEquals(listenerDown.expectOnResult(), TEST_IFACE);
+
+        final boolean retUp =
+                mNetFactory.updateInterfaceLinkState(TEST_IFACE, true /* up */, listenerUp);
+
+        assertTrue(retUp);
+        assertEquals(listenerUp.expectOnResult(), TEST_IFACE);
     }
 
     @Test
@@ -726,4 +733,16 @@
         triggerOnProvisioningSuccess();
         verifyRestart(initialIpConfig);
     }
+
+    @Test
+    public void testOnNetworkNeededOnStaleNetworkOffer() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+        mNetFactory.updateInterfaceLinkState(TEST_IFACE, false, null);
+        verify(mNetworkProvider).unregisterNetworkOffer(mNetworkOfferCallback);
+        // It is possible that even after a network offer is unregistered, CS still sends it
+        // onNetworkNeeded() callbacks.
+        mNetworkOfferCallback.onNetworkNeeded(createDefaultRequest());
+        verify(mIpClient, never()).startProvisioning(any());
+    }
 }
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
index 5400a00..f6fb45c 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
@@ -44,6 +44,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.frameworks.tests.net.R;
+import com.android.server.BpfNetMaps;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 
@@ -74,6 +75,7 @@
     private File mTestProc;
     private NetworkStatsFactory mFactory;
     @Mock private Context mContext;
+    @Mock private BpfNetMaps mBpfNetMaps;
 
     @Before
     public void setUp() throws Exception {
@@ -84,7 +86,7 @@
         // applications. So in order to have a test support native library, the native code
         // related to networkStatsFactory is compiled to a minimal native library and loaded here.
         System.loadLibrary("networkstatsfactorytestjni");
-        mFactory = new NetworkStatsFactory(mContext, mTestProc, false);
+        mFactory = new NetworkStatsFactory(mContext, mTestProc, false, mBpfNetMaps);
         mFactory.updateUnderlyingNetworkInfos(new UnderlyingNetworkInfo[0]);
     }