Merge "[SettingsProvider] fix connectivity test"
diff --git a/TEST_MAPPING b/TEST_MAPPING
index a5b97a1..95f854b 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -3,6 +3,10 @@
     {
       "name": "ConnectivityCoverageTests"
     },
+    {
+      // In addition to ConnectivityCoverageTests, runs non-connectivity-module tests
+      "name": "FrameworksNetTests"
+    },
     // Run in addition to mainline-presubmit as mainline-presubmit is not
     // supported in every branch.
     // CtsNetTestCasesLatestSdk uses stable API shims, so does not exercise
@@ -22,6 +26,9 @@
       "name": "bpf_existence_test"
     },
     {
+      "name": "connectivity_native_test"
+    },
+    {
       "name": "netd_updatable_unit_test"
     },
     {
@@ -78,6 +85,9 @@
       "name": "bpf_existence_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
     },
     {
+      "name": "connectivity_native_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
+    },
+    {
       "name": "netd_updatable_unit_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
     },
     {
@@ -127,9 +137,6 @@
           "exclude-annotation": "androidx.test.filters.RequiresDevice"
         }
       ]
-    },
-    {
-      "name": "TetheringCoverageTests[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
     }
   ],
   "auto-postsubmit": [
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index dd04d6c..bd8fe7c 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -69,9 +69,10 @@
     ],
     canned_fs_config: "canned_fs_config",
     bpfs: [
+        "block.o",
         "clatd.o_mainline",
-        "netd.o_mainline",
         "dscp_policy.o",
+        "netd.o_mainline",
         "offload.o",
         "test.o",
     ],
@@ -89,6 +90,8 @@
     compressible: true,
 
     androidManifest: "AndroidManifest.xml",
+
+    compat_configs: ["connectivity-platform-compat-config"],
 }
 
 apex_key {
@@ -131,15 +134,41 @@
     hidden_api: {
         max_target_r_low_priority: [
             "hiddenapi/hiddenapi-max-target-r-loprio.txt",
-	],
+        ],
         max_target_o_low_priority: [
             "hiddenapi/hiddenapi-max-target-o-low-priority.txt",
             "hiddenapi/hiddenapi-max-target-o-low-priority-tiramisu.txt",
-	],
+        ],
         unsupported: [
             "hiddenapi/hiddenapi-unsupported.txt",
             "hiddenapi/hiddenapi-unsupported-tiramisu.txt",
         ],
+
+        // The following packages contain classes from other modules on the
+        // bootclasspath. That means that the hidden API flags for this module
+        // has to explicitly list every single class this module provides in
+        // that package to differentiate them from the classes provided by other
+        // modules. That can include private classes that are not part of the
+        // API.
+        split_packages: [
+            "android.app.usage",
+            "android.net",
+            "android.net.netstats",
+            "android.net.util",
+        ],
+
+        // The following packages and all their subpackages currently only
+        // contain classes from this bootclasspath_fragment. Listing a package
+        // here won't prevent other bootclasspath modules from adding classes in
+        // any of those packages but it will prevent them from adding those
+        // classes into an API surface, e.g. public, system, etc.. Doing so will
+        // result in a build failure due to inconsistent flags.
+        package_prefixes: [
+            "android.net.apf",
+            "android.net.connectivity",
+            "android.net.netstats.provider",
+            "android.net.nsd",
+        ],
     },
 }
 
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 22d2c5d..b865a8e 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
@@ -30,9 +30,9 @@
 import com.android.net.module.util.IBpfMap.ThrowingBiConsumer;
 import com.android.net.module.util.bpf.Tether4Key;
 import com.android.net.module.util.bpf.Tether4Value;
+import com.android.net.module.util.bpf.TetherStatsValue;
 import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
-import com.android.networkstack.tethering.TetherStatsValue;
 
 /**
  * Bpf coordinator class for API shims.
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 5afb862..0683e5e 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
@@ -33,6 +33,8 @@
 import com.android.net.module.util.IBpfMap.ThrowingBiConsumer;
 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.networkstack.tethering.BpfCoordinator.Dependencies;
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
 import com.android.networkstack.tethering.BpfUtils;
@@ -42,8 +44,6 @@
 import com.android.networkstack.tethering.TetherDownstream6Key;
 import com.android.networkstack.tethering.TetherLimitKey;
 import com.android.networkstack.tethering.TetherLimitValue;
-import com.android.networkstack.tethering.TetherStatsKey;
-import com.android.networkstack.tethering.TetherStatsValue;
 import com.android.networkstack.tethering.TetherUpstream6Key;
 
 import java.io.FileDescriptor;
diff --git a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
index 915e210..69cbab5 100644
--- a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
+++ b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
@@ -25,9 +25,9 @@
 import com.android.net.module.util.IBpfMap.ThrowingBiConsumer;
 import com.android.net.module.util.bpf.Tether4Key;
 import com.android.net.module.util.bpf.Tether4Value;
+import com.android.net.module.util.bpf.TetherStatsValue;
 import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
-import com.android.networkstack.tethering.TetherStatsValue;
 
 /**
  * Bpf coordinator class for API shims.
diff --git a/Tethering/common/TetheringLib/src/android/net/ITetheringEventCallback.aidl b/Tethering/common/TetheringLib/src/android/net/ITetheringEventCallback.aidl
index b4e3ba4..836761f 100644
--- a/Tethering/common/TetheringLib/src/android/net/ITetheringEventCallback.aidl
+++ b/Tethering/common/TetheringLib/src/android/net/ITetheringEventCallback.aidl
@@ -36,4 +36,5 @@
     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 253eacb..f33f846 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 {
-    boolean tetheringSupported;
+    long supportedTypes;
     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 6f9b33e..b3f0cf2 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -183,6 +183,12 @@
      */
     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 = {
@@ -520,6 +526,9 @@
         }
 
         @Override
+        public void onSupportedTetheringTypes(long supportedBitmap) { }
+
+        @Override
         public void onUpstreamChanged(Network network) { }
 
         @Override
@@ -1033,15 +1042,29 @@
         /**
          * 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 supported The new supported status
+         * @param supportedTypes a set of @TetheringType which is supported.
+         * @hide
          */
-        default void onTetheringSupported(boolean supported) {}
+        default void onSupportedTetheringTypes(@NonNull Set<Integer> supportedTypes) {}
 
         /**
          * Called when tethering upstream changed.
@@ -1339,7 +1362,8 @@
                 @Override
                 public void onCallbackStarted(TetheringCallbackStartedParcel parcel) {
                     executor.execute(() -> {
-                        callback.onTetheringSupported(parcel.tetheringSupported);
+                        callback.onSupportedTetheringTypes(unpackBits(parcel.supportedTypes));
+                        callback.onTetheringSupported(parcel.supportedTypes != 0);
                         callback.onUpstreamChanged(parcel.upstreamNetwork);
                         sendErrorCallbacks(parcel.states);
                         sendRegexpsChanged(parcel.config);
@@ -1358,6 +1382,13 @@
                     });
                 }
 
+                @Override
+                public void onSupportedTetheringTypes(long supportedBitmap) {
+                    executor.execute(() -> {
+                        callback.onSupportedTetheringTypes(unpackBits(supportedBitmap));
+                    });
+                }
+
                 private void sendRegexpsChanged(TetheringConfigurationParcel parcel) {
                     callback.onTetherableInterfaceRegexpsChanged(new TetheringInterfaceRegexps(
                             parcel.tetherableBluetoothRegexs,
@@ -1396,6 +1427,23 @@
     }
 
     /**
+     * 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/proguard.flags b/Tethering/proguard.flags
index 7b5ae0d..2905e28 100644
--- a/Tethering/proguard.flags
+++ b/Tethering/proguard.flags
@@ -12,6 +12,11 @@
     native <methods>;
 }
 
+# Ensure runtime-visible field annotations are kept when using R8 full mode.
+-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
+-keep interface com.android.networkstack.tethering.util.Struct$Field {
+    *;
+}
 -keepclassmembers public class * extends com.android.networkstack.tethering.util.Struct {
     *;
 }
diff --git a/Tethering/res/values/config.xml b/Tethering/res/values/config.xml
index 0412a49..bfec5bc 100644
--- a/Tethering/res/values/config.xml
+++ b/Tethering/res/values/config.xml
@@ -78,6 +78,12 @@
     <!-- Use legacy wifi p2p dedicated address instead of randomize address. -->
     <bool translatable="false" name="config_tether_enable_legacy_wifi_p2p_dedicated_ip">false</bool>
 
+    <!-- Use lease subnet prefix length to reserve the range outside of subnet prefix length.
+         This configuration only valid if its value larger than dhcp server address prefix length
+         and config_tether_enable_legacy_wifi_p2p_dedicated_ip is true.
+    -->
+    <integer translatable="false" name="config_p2p_leases_subnet_prefix_length">0</integer>
+
     <!-- Dhcp range (min, max) to use for tethering purposes -->
     <string-array translatable="false" name="config_tether_dhcp_range">
     </string-array>
diff --git a/Tethering/res/values/overlayable.xml b/Tethering/res/values/overlayable.xml
index 91fbd7d..7bd905c 100644
--- a/Tethering/res/values/overlayable.xml
+++ b/Tethering/res/values/overlayable.xml
@@ -32,6 +32,7 @@
             <item type="bool" name="config_tether_enable_bpf_offload"/>
             <item type="bool" name="config_tether_enable_legacy_dhcp_server"/>
             <item type="bool" name="config_tether_enable_legacy_wifi_p2p_dedicated_ip"/>
+            <item type="integer" name="config_p2p_leases_subnet_prefix_length"/>
             <item type="integer" name="config_tether_offload_poll_interval"/>
             <item type="array" name="config_tether_upstream_types"/>
             <item type="bool" name="config_tether_upstream_automatic"/>
diff --git a/Tethering/src/android/net/dhcp/DhcpServingParamsParcelExt.java b/Tethering/src/android/net/dhcp/DhcpServingParamsParcelExt.java
index aaaec17..8d58945 100644
--- a/Tethering/src/android/net/dhcp/DhcpServingParamsParcelExt.java
+++ b/Tethering/src/android/net/dhcp/DhcpServingParamsParcelExt.java
@@ -185,6 +185,16 @@
         return this;
     }
 
+    /** Set leases subnet prefix length. If the value is smaller than server address prefix length,
+     * this configuration will be ignored.
+     *
+     * <p>If not set, the default value is zero.
+     */
+    public DhcpServingParamsParcelExt setLeasesSubnetPrefixLength(int prefixLength) {
+        this.leasesSubnetPrefixLength = prefixLength;
+        return this;
+    }
+
     private static int[] toIntArray(@NonNull Collection<Inet4Address> addrs) {
         int[] res = new int[addrs.size()];
         int i = 0;
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index acd2625..c718f4c 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -241,6 +241,7 @@
     private final LinkProperties mLinkProperties;
     private final boolean mUsingLegacyDhcp;
     private final boolean mUsingBpfOffload;
+    private final int mP2pLeasesSubnetPrefixLength;
 
     private final Dependencies mDeps;
 
@@ -299,6 +300,7 @@
         mLinkProperties = new LinkProperties();
         mUsingLegacyDhcp = config.useLegacyDhcpServer();
         mUsingBpfOffload = config.isBpfOffloadEnabled();
+        mP2pLeasesSubnetPrefixLength = config.getP2pLeasesSubnetPrefixLength();
         mPrivateAddressCoordinator = addressCoordinator;
         mDeps = deps;
         resetLinkProperties();
@@ -527,6 +529,9 @@
             @Nullable Inet4Address clientAddr) {
         final boolean changePrefixOnDecline =
                 (mInterfaceType == TetheringManager.TETHERING_NCM && clientAddr == null);
+        final int subnetPrefixLength = mInterfaceType == TetheringManager.TETHERING_WIFI_P2P
+                ? mP2pLeasesSubnetPrefixLength : 0 /* default value */;
+
         return new DhcpServingParamsParcelExt()
             .setDefaultRouters(defaultRouter)
             .setDhcpLeaseTimeSecs(DHCP_LEASE_TIME_SECS)
@@ -534,7 +539,8 @@
             .setServerAddr(serverAddr)
             .setMetered(true)
             .setSingleClientAddr(clientAddr)
-            .setChangePrefixOnDecline(changePrefixOnDecline);
+            .setChangePrefixOnDecline(changePrefixOnDecline)
+            .setLeasesSubnetPrefixLength(subnetPrefixLength);
             // TODO: also advertise link MTU
     }
 
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index f8a1094..ecb6478 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -65,9 +65,12 @@
 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.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.netlink.ConntrackMessage;
 import com.android.net.module.util.netlink.NetlinkConstants;
 import com.android.net.module.util.netlink.NetlinkSocket;
@@ -118,6 +121,8 @@
     private static final String TETHER_LIMIT_MAP_PATH = makeMapPath("limit");
     private static final String TETHER_ERROR_MAP_PATH = makeMapPath("error");
     private static final String TETHER_DEV_MAP_PATH = makeMapPath("dev");
+    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 = ",";
@@ -1072,7 +1077,8 @@
         }
     }
 
-    private String ipv4RuleToBase64String(Tether4Key key, Tether4Value value) {
+    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", "");
@@ -1083,28 +1089,45 @@
         return keyBase64Str + DUMP_BASE64_DELIMITER + valueBase64Str;
     }
 
-    private void dumpRawIpv4ForwardingRuleMap(
-            BpfMap<Tether4Key, Tether4Value> map, IndentingPrintWriter pw) throws ErrnoException {
+    private <K extends Struct, V extends Struct> void dumpRawMap(BpfMap<K, V> map,
+            IndentingPrintWriter pw) throws ErrnoException {
         if (map == null) {
-            pw.println("No IPv4 support");
+            pw.println("No BPF support");
             return;
         }
         if (map.isEmpty()) {
-            pw.println("No rules");
+            pw.println("No entries");
             return;
         }
-        map.forEach((k, v) -> pw.println(ipv4RuleToBase64String(k, v)));
+        map.forEach((k, v) -> pw.println(bpfMapEntryToBase64String(k, v)));
     }
 
     /**
      * Dump raw BPF map in base64 encoded strings. For test only.
+     * Only allow to dump one map path once.
+     * Format:
+     * $ dumpsys tethering bpfRawMap --<map name>
      */
-    public void dumpRawMap(@NonNull IndentingPrintWriter pw) {
-        try (BpfMap<Tether4Key, Tether4Value> upstreamMap = mDeps.getBpfUpstream4Map()) {
-            // TODO: dump downstream map.
-            dumpRawIpv4ForwardingRuleMap(upstreamMap, pw);
-        } catch (ErrnoException e) {
-            pw.println("Error dumping IPv4 map: " + e);
+    public void dumpRawMap(@NonNull IndentingPrintWriter pw, @Nullable String[] args) {
+        // TODO: consider checking the arg order that <map name> is after "bpfRawMap". Probably
+        // it is okay for now because this is used by test only and test is supposed to use
+        // expected argument order.
+        // TODO: dump downstream4 map.
+        if (CollectionUtils.contains(args, DUMPSYS_RAWMAP_ARG_STATS)) {
+            try (BpfMap<TetherStatsKey, TetherStatsValue> statsMap = mDeps.getBpfStatsMap()) {
+                dumpRawMap(statsMap, pw);
+            } catch (ErrnoException e) {
+                pw.println("Error dumping stats map: " + e);
+            }
+            return;
+        }
+        if (CollectionUtils.contains(args, DUMPSYS_RAWMAP_ARG_UPSTREAM4)) {
+            try (BpfMap<Tether4Key, Tether4Value> upstreamMap = mDeps.getBpfUpstream4Map()) {
+                dumpRawMap(upstreamMap, pw);
+            } catch (ErrnoException e) {
+                pw.println("Error dumping IPv4 map: " + e);
+            }
+            return;
         }
     }
 
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherStatsKey.java b/Tethering/src/com/android/networkstack/tethering/TetherStatsKey.java
deleted file mode 100644
index 5442480..0000000
--- a/Tethering/src/com/android/networkstack/tethering/TetherStatsKey.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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.networkstack.tethering;
-
-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/Tethering/src/com/android/networkstack/tethering/TetherStatsValue.java b/Tethering/src/com/android/networkstack/tethering/TetherStatsValue.java
deleted file mode 100644
index 844d2e8..0000000
--- a/Tethering/src/com/android/networkstack/tethering/TetherStatsValue.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * 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.networkstack.tethering;
-
-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/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 0b607bd..4504829 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -135,6 +135,7 @@
 import com.android.internal.util.StateMachine;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
+import com.android.net.module.util.CollectionUtils;
 import com.android.networkstack.apishim.common.BluetoothPanShim;
 import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceCallbackShim;
 import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceRequestShim;
@@ -278,6 +279,11 @@
     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");
@@ -494,6 +500,8 @@
         mUpstreamNetworkMonitor.setUpstreamConfig(mConfig.chooseUpstreamAutomatically,
                 mConfig.isDunRequired);
         reportConfigurationChanged(mConfig.toStableParcelable());
+
+        updateSupportedDownstreams(mConfig);
     }
 
     private void maybeDunSettingChanged() {
@@ -1513,26 +1521,6 @@
         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;
     }
@@ -2322,7 +2310,7 @@
         mHandler.post(() -> {
             mTetheringEventCallbacks.register(callback, new CallbackCookie(hasListPermission));
             final TetheringCallbackStartedParcel parcel = new TetheringCallbackStartedParcel();
-            parcel.tetheringSupported = isTetheringSupported();
+            parcel.supportedTypes = mSupportedTypeBitmap;
             parcel.upstreamNetwork = mTetherUpstream;
             parcel.config = mConfig.toStableParcelable();
             parcel.states =
@@ -2361,6 +2349,22 @@
         });
     }
 
+    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;
@@ -2445,18 +2449,56 @@
         }
     }
 
+    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.
-    boolean isTetheringSupported() {
+    private boolean isTetheringAllowed() {
         final int defaultVal = mDeps.isTetheringDenied() ? 0 : 1;
         final boolean tetherSupported = Settings.Global.getInt(mContext.getContentResolver(),
                 Settings.Global.TETHER_SUPPORTED, defaultVal) != 0;
-        final boolean tetherEnabledInSettings = tetherSupported
+        return tetherSupported
                 && !mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_TETHERING);
+    }
 
-        return tetherEnabledInSettings && hasAnySupportedDownstream()
-                && !mEntitlementMgr.isProvisioningNeededButUnavailable();
+    boolean isTetheringSupported() {
+        return mSupportedTypeBitmap > 0;
     }
 
     private void dumpBpf(IndentingPrintWriter pw) {
@@ -2472,13 +2514,12 @@
                 writer, "  ");
 
         // Used for testing instead of human debug.
-        // TODO: add options to choose which map to dump.
-        if (argsContain(args, "bpfRawMap")) {
-            mBpfCoordinator.dumpRawMap(pw);
+        if (CollectionUtils.contains(args, "bpfRawMap")) {
+            mBpfCoordinator.dumpRawMap(pw, args);
             return;
         }
 
-        if (argsContain(args, "bpf")) {
+        if (CollectionUtils.contains(args, "bpf")) {
             dumpBpf(pw);
             return;
         }
@@ -2544,7 +2585,7 @@
 
         pw.println("Log:");
         pw.increaseIndent();
-        if (argsContain(args, "--short")) {
+        if (CollectionUtils.contains(args, "--short")) {
             pw.println("<log removed for brevity>");
         } else {
             mLog.dump(fd, pw, args);
@@ -2588,13 +2629,6 @@
         if (e != null) throw e;
     }
 
-    private static boolean argsContain(String[] args, String target) {
-        for (String arg : args) {
-            if (target.equals(arg)) return true;
-        }
-        return false;
-    }
-
     private void updateConnectedClients(final List<WifiClient> wifiClients) {
         if (mConnectedClientsTracker.updateConnectedClients(mTetherMainSM.getAllDownstreams(),
                 wifiClients)) {
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
index eaf8589..f9f3ed9 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
@@ -149,6 +149,7 @@
     // TODO: Add to TetheringConfigurationParcel if required.
     private final boolean mEnableBpfOffload;
     private final boolean mEnableWifiP2pDedicatedIp;
+    private final int mP2pLeasesSubnetPrefixLength;
 
     private final int mUsbTetheringFunction;
     protected final ContentResolver mContentResolver;
@@ -214,9 +215,27 @@
                 R.bool.config_tether_enable_legacy_wifi_p2p_dedicated_ip,
                 false /* defaultValue */);
 
+        mP2pLeasesSubnetPrefixLength = getP2pLeasesSubnetPrefixLengthFromRes(res, configLog);
+
         configLog.log(toString());
     }
 
+    private int getP2pLeasesSubnetPrefixLengthFromRes(final Resources res, final SharedLog log) {
+        if (!mEnableWifiP2pDedicatedIp) return 0;
+
+        int prefixLength = getResourceInteger(res,
+                R.integer.config_p2p_leases_subnet_prefix_length, 0 /* default value */);
+
+        // DhcpLeaseRepository ignores the first and last addresses of the range so the max prefix
+        // length is 30.
+        if (prefixLength < 0 || prefixLength > 30) {
+            log.e("Invalid p2p leases subnet prefix length configuration: " + prefixLength);
+            return 0;
+        }
+
+        return prefixLength;
+    }
+
     /** Check whether using legacy dhcp server. */
     public boolean useLegacyDhcpServer() {
         return mEnableLegacyDhcpServer;
@@ -272,6 +291,15 @@
         return mEnableWifiP2pDedicatedIp;
     }
 
+    /**
+     * Get subnet prefix length of dhcp leases for wifi p2p.
+     * This feature only support when wifi p2p use dedicated address. If
+     * #shouldEnableWifiP2pDedicatedIp is false, this method would always return 0.
+     */
+    public int getP2pLeasesSubnetPrefixLength() {
+        return mP2pLeasesSubnetPrefixLength;
+    }
+
     /** Does the dumping.*/
     public void dump(PrintWriter pw) {
         pw.print("activeDataSubId: ");
@@ -310,6 +338,9 @@
         pw.print("enableWifiP2pDedicatedIp: ");
         pw.println(mEnableWifiP2pDedicatedIp);
 
+        pw.print("p2pLeasesSubnetPrefixLength: ");
+        pw.println(mP2pLeasesSubnetPrefixLength);
+
         pw.print("mUsbTetheringFunction: ");
         pw.println(isUsingNcm() ? "NCM" : "RNDIS");
     }
diff --git a/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java b/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java
index 66d67a1..e6236df 100644
--- a/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java
+++ b/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java
@@ -22,7 +22,7 @@
 import androidx.annotation.NonNull;
 
 import com.android.net.module.util.JniUtil;
-import com.android.networkstack.tethering.TetherStatsValue;
+import com.android.net.module.util.bpf.TetherStatsValue;
 
 import java.io.FileDescriptor;
 import java.net.Inet6Address;
diff --git a/Tethering/tests/integration/Android.bp b/Tethering/tests/integration/Android.bp
index 6eaf68b..a4d0448 100644
--- a/Tethering/tests/integration/Android.bp
+++ b/Tethering/tests/integration/Android.bp
@@ -83,68 +83,3 @@
     compile_multilib: "both",
     jarjar_rules: ":NetworkStackJarJarRules",
 }
-
-android_library {
-    name: "TetheringCoverageTestsLib",
-    min_sdk_version: "30",
-    static_libs: [
-        "NetdStaticLibTestsLib",
-        "NetworkStaticLibTestsLib",
-        "NetworkStackTestsLib",
-        "TetheringTestsLatestSdkLib",
-        "TetheringIntegrationTestsLatestSdkLib",
-    ],
-    // Jarjar rules should normally be applied on final artifacts and not intermediate libraries as
-    // applying different rules on intermediate libraries can cause conflicts when combining them
-    // (the resulting artifact can end up with multiple incompatible implementations of the same
-    // classes). But this library is used to combine tethering coverage tests with connectivity
-    // coverage tests into a single coverage target. The tests need to use the same jarjar rules as
-    // covered production code for coverage to be calculated properly, so jarjar is applied
-    // separately on each set of tests.
-    jarjar_rules: ":TetheringCoverageJarJarRules",
-    manifest: "AndroidManifest_coverage.xml",
-    visibility: [
-        "//packages/modules/Connectivity/tests:__subpackages__"
-    ],
-}
-
-// Combine NetworkStack and Tethering jarjar rules for coverage target. The jarjar files are
-// simply concatenated in the order specified in srcs.
-genrule {
-    name: "TetheringCoverageJarJarRules",
-    srcs: [
-        ":TetheringTestsJarJarRules",
-        ":NetworkStackJarJarRules",
-    ],
-    out: ["jarjar-rules-tethering-coverage.txt"],
-    cmd: "cat $(in) > $(out)",
-    visibility: ["//visibility:private"],
-}
-
-// Special version of the tethering tests that includes all tests necessary for code coverage
-// purposes. This is currently the union of TetheringTests, TetheringIntegrationTests and
-// NetworkStackTests.
-// TODO: remove in favor of ConnectivityCoverageTests, which includes below tests and more
-android_test {
-    name: "TetheringCoverageTests",
-    platform_apis: true,
-    min_sdk_version: "30",
-    target_sdk_version: "31",
-    test_suites: ["device-tests", "mts-tethering"],
-    test_config: "AndroidTest_Coverage.xml",
-    defaults: ["libnetworkstackutilsjni_deps"],
-    static_libs: [
-        "modules-utils-native-coverage-listener",
-        "TetheringCoverageTestsLib",
-    ],
-    jni_libs: [
-        // For mockito extended
-        "libdexmakerjvmtiagent",
-        "libstaticjvmtiagent",
-        // For NetworkStackUtils included in NetworkStackBase
-        "libnetworkstackutilsjni",
-        "libcom_android_networkstack_tethering_util_jni",
-    ],
-    compile_multilib: "both",
-    manifest: "AndroidManifest_coverage.xml",
-}
diff --git a/Tethering/tests/integration/AndroidManifest_coverage.xml b/Tethering/tests/integration/AndroidManifest_coverage.xml
deleted file mode 100644
index 06de00d..0000000
--- a/Tethering/tests/integration/AndroidManifest_coverage.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          xmlns:tools="http://schemas.android.com/tools"
-          package="com.android.networkstack.tethering.tests.coverage">
-
-    <application tools:replace="android:label"
-                 android:debuggable="true"
-                 android:label="Tethering coverage tests">
-        <uses-library android:name="android.test.runner" />
-    </application>
-    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.networkstack.tethering.tests.coverage"
-                     android:label="Tethering coverage tests">
-    </instrumentation>
-</manifest>
diff --git a/Tethering/tests/integration/AndroidTest_Coverage.xml b/Tethering/tests/integration/AndroidTest_Coverage.xml
deleted file mode 100644
index 33c5b3d..0000000
--- a/Tethering/tests/integration/AndroidTest_Coverage.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-<configuration description="Runs coverage tests for Tethering">
-    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
-        <option name="test-file-name" value="TetheringCoverageTests.apk" />
-    </target_preparer>
-
-    <option name="test-tag" value="TetheringCoverageTests" />
-    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
-        <option name="package" value="com.android.networkstack.tethering.tests.coverage" />
-        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
-        <option name="hidden-api-checks" value="false"/>
-        <option name="device-listeners" value="com.android.modules.utils.testing.NativeCoverageHackInstrumentationListener" />
-    </test>
-</configuration>
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index de81a38..e73b7d5 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -74,6 +74,8 @@
 import com.android.net.module.util.Struct;
 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.structs.EthernetHeader;
 import com.android.net.module.util.structs.Icmpv6Header;
 import com.android.net.module.util.structs.Ipv4Header;
@@ -128,6 +130,13 @@
     // Kernel treats a confirmed UDP connection which active after two seconds as stream mode.
     // See upstream commit b7b1d02fc43925a4d569ec221715db2dfa1ce4f5.
     private static final int UDP_STREAM_TS_MS = 2000;
+    // Per RX UDP packet size: iphdr (20) + udphdr (8) + payload (2) = 30 bytes.
+    private static final int RX_UDP_PACKET_SIZE = 30;
+    private static final int RX_UDP_PACKET_COUNT = 456;
+    // Per TX UDP packet size: ethhdr (14) + iphdr (20) + udphdr (8) + payload (2) = 44 bytes.
+    private static final int TX_UDP_PACKET_SIZE = 44;
+    private static final int TX_UDP_PACKET_COUNT = 123;
+
     private static final LinkAddress TEST_IP4_ADDR = new LinkAddress("10.0.0.1/8");
     private static final LinkAddress TEST_IP6_ADDR = new LinkAddress("2001:db8:1::101/64");
     private static final InetAddress TEST_IP4_DNS = parseNumericAddress("8.8.8.8");
@@ -136,6 +145,8 @@
             ByteBuffer.wrap(new byte[] { (byte) 0x55, (byte) 0xaa });
 
     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";
 
@@ -168,12 +179,13 @@
         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);
     }
 
@@ -201,7 +213,6 @@
             mHandler.post(() -> reader.stop());
             mDownstreamReader = null;
         }
-        mHandlerThread.quitSafely();
         mTetheredInterfaceRequester.release();
         mEm.setIncludeTestInterfaces(false);
         maybeDeleteTestInterface();
@@ -212,6 +223,7 @@
         try {
             if (mRunTests) cleanUp();
         } finally {
+            mHandlerThread.quitSafely();
             mUiAutomation.dropShellPermissionIdentity();
         }
     }
@@ -400,6 +412,23 @@
         // 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);
@@ -938,27 +967,69 @@
                 return isExpectedUdpPacket(p, false /* hasEther */, PAYLOAD3);
             });
 
-            final HashMap<Tether4Key, Tether4Value> upstreamMap = pollIpv4UpstreamMapFromDump();
+            // [1] Verify IPv4 upstream rule map.
+            final HashMap<Tether4Key, Tether4Value> upstreamMap = pollRawMapFromDump(
+                    Tether4Key.class, Tether4Value.class, DUMPSYS_RAWMAP_ARG_UPSTREAM4);
             assertNotNull(upstreamMap);
             assertEquals(1, upstreamMap.size());
 
             final Map.Entry<Tether4Key, Tether4Value> rule =
                     upstreamMap.entrySet().iterator().next();
 
-            final Tether4Key key = rule.getKey();
-            assertEquals(IPPROTO_UDP, key.l4proto);
-            assertTrue(Arrays.equals(tethered.ipv4Addr.getAddress(), key.src4));
-            assertEquals(LOCAL_PORT, key.srcPort);
-            assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(), key.dst4));
-            assertEquals(REMOTE_PORT, key.dstPort);
+            final Tether4Key upstream4Key = rule.getKey();
+            assertEquals(IPPROTO_UDP, upstream4Key.l4proto);
+            assertTrue(Arrays.equals(tethered.ipv4Addr.getAddress(), upstream4Key.src4));
+            assertEquals(LOCAL_PORT, upstream4Key.srcPort);
+            assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(), upstream4Key.dst4));
+            assertEquals(REMOTE_PORT, upstream4Key.dstPort);
 
-            final Tether4Value value = rule.getValue();
+            final Tether4Value upstream4Value = rule.getValue();
             assertTrue(Arrays.equals(publicIp4Addr.getAddress(),
-                    InetAddress.getByAddress(value.src46).getAddress()));
-            assertEquals(LOCAL_PORT, value.srcPort);
+                    InetAddress.getByAddress(upstream4Value.src46).getAddress()));
+            assertEquals(LOCAL_PORT, upstream4Value.srcPort);
             assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(),
-                    InetAddress.getByAddress(value.dst46).getAddress()));
-            assertEquals(REMOTE_PORT, value.dstPort);
+                    InetAddress.getByAddress(upstream4Value.dst46).getAddress()));
+            assertEquals(REMOTE_PORT, upstream4Value.dstPort);
+
+            // [2] Verify stats map.
+            // Transmit packets on both direction for verifying stats. Because we only care the
+            // packet count in stats test, we just reuse the existing packets to increaes
+            // the packet count on both direction.
+
+            // Send packets on original direction.
+            for (int i = 0; i < TX_UDP_PACKET_COUNT; i++) {
+                tester.verifyUpload(remote, originalPacket, p -> {
+                    Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
+                    return isExpectedUdpPacket(p, false /* hasEther */, PAYLOAD);
+                });
+            }
+
+            // Send packets on reply direction.
+            for (int i = 0; i < RX_UDP_PACKET_COUNT; i++) {
+                remote.verifyDownload(tester, replyPacket, p -> {
+                    Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
+                    return isExpectedUdpPacket(p, true/* hasEther */, PAYLOAD2);
+                });
+            }
+
+            // Dump stats map to verify.
+            final HashMap<TetherStatsKey, TetherStatsValue> statsMap = pollRawMapFromDump(
+                    TetherStatsKey.class, TetherStatsValue.class, DUMPSYS_RAWMAP_ARG_STATS);
+            assertNotNull(statsMap);
+            assertEquals(1, statsMap.size());
+
+            final Map.Entry<TetherStatsKey, TetherStatsValue> stats =
+                    statsMap.entrySet().iterator().next();
+
+            // TODO: verify the upstream index in TetherStatsKey.
+
+            final TetherStatsValue statsValue = stats.getValue();
+            assertEquals(RX_UDP_PACKET_COUNT, statsValue.rxPackets);
+            assertEquals(RX_UDP_PACKET_COUNT * RX_UDP_PACKET_SIZE, statsValue.rxBytes);
+            assertEquals(0, statsValue.rxErrors);
+            assertEquals(TX_UDP_PACKET_COUNT, statsValue.txPackets);
+            assertEquals(TX_UDP_PACKET_COUNT * TX_UDP_PACKET_SIZE, statsValue.txBytes);
+            assertEquals(0, statsValue.txErrors);
         }
     }
 
@@ -1003,7 +1074,8 @@
     }
 
     @Nullable
-    private Pair<Tether4Key, Tether4Value> parseTether4KeyValue(@NonNull String dumpStr) {
+    private <K extends Struct, V extends Struct> Pair<K, V> parseMapKeyValue(
+            Class<K> keyClass, Class<V> valueClass, @NonNull String dumpStr) {
         Log.w(TAG, "Parsing string: " + dumpStr);
 
         String[] keyValueStrs = dumpStr.split(BASE64_DELIMITER);
@@ -1016,36 +1088,38 @@
         Log.d(TAG, "keyBytes: " + dumpHexString(keyBytes));
         final ByteBuffer keyByteBuffer = ByteBuffer.wrap(keyBytes);
         keyByteBuffer.order(ByteOrder.nativeOrder());
-        final Tether4Key tether4Key = Struct.parse(Tether4Key.class, keyByteBuffer);
-        Log.w(TAG, "tether4Key: " + tether4Key);
+        final K k = Struct.parse(keyClass, keyByteBuffer);
 
         final byte[] valueBytes = Base64.decode(keyValueStrs[1], Base64.DEFAULT);
         Log.d(TAG, "valueBytes: " + dumpHexString(valueBytes));
         final ByteBuffer valueByteBuffer = ByteBuffer.wrap(valueBytes);
         valueByteBuffer.order(ByteOrder.nativeOrder());
-        final Tether4Value tether4Value = Struct.parse(Tether4Value.class, valueByteBuffer);
-        Log.w(TAG, "tether4Value: " + tether4Value);
+        final V v = Struct.parse(valueClass, valueByteBuffer);
 
-        return new Pair<>(tether4Key, tether4Value);
+        return new Pair<>(k, v);
     }
 
     @NonNull
-    private HashMap<Tether4Key, Tether4Value> dumpIpv4UpstreamMap() throws Exception {
-        final String rawMapStr = DumpTestUtils.dumpService(Context.TETHERING_SERVICE,
-                DUMPSYS_TETHERING_RAWMAP_ARG);
-        final HashMap<Tether4Key, Tether4Value> map = new HashMap<>();
+    private <K extends Struct, V extends Struct> HashMap<K, V> dumpAndParseRawMap(
+            Class<K> keyClass, Class<V> valueClass, @NonNull String mapArg)
+            throws Exception {
+        final String[] args = new String[] {DUMPSYS_TETHERING_RAWMAP_ARG, mapArg};
+        final String rawMapStr = DumpTestUtils.dumpService(Context.TETHERING_SERVICE, args);
+        final HashMap<K, V> map = new HashMap<>();
 
         for (final String line : rawMapStr.split(LINE_DELIMITER)) {
-            final Pair<Tether4Key, Tether4Value> rule = parseTether4KeyValue(line.trim());
+            final Pair<K, V> rule = parseMapKeyValue(keyClass, valueClass, line.trim());
             map.put(rule.first, rule.second);
         }
         return map;
     }
 
     @Nullable
-    private HashMap<Tether4Key, Tether4Value> pollIpv4UpstreamMapFromDump() throws Exception {
+    private <K extends Struct, V extends Struct> HashMap<K, V> pollRawMapFromDump(
+            Class<K> keyClass, Class<V> valueClass, @NonNull String mapArg)
+            throws Exception {
         for (int retryCount = 0; retryCount < DUMP_POLLING_MAX_RETRY; retryCount++) {
-            final HashMap<Tether4Key, Tether4Value> map = dumpIpv4UpstreamMap();
+            final HashMap<K, V> map = dumpAndParseRawMap(keyClass, valueClass, mapArg);
             if (!map.isEmpty()) return map;
 
             Thread.sleep(DUMP_POLLING_INTERVAL_MS);
diff --git a/Tethering/tests/jarjar-rules.txt b/Tethering/tests/jarjar-rules.txt
index a7c7488..cd8fd3a 100644
--- a/Tethering/tests/jarjar-rules.txt
+++ b/Tethering/tests/jarjar-rules.txt
@@ -7,6 +7,8 @@
 rule com.android.internal.util.State* com.android.networkstack.tethering.util.State@1
 rule com.android.internal.util.StateMachine* com.android.networkstack.tethering.util.StateMachine@1
 rule com.android.internal.util.TrafficStatsConstants* com.android.networkstack.tethering.util.TrafficStatsConstants@1
+# Keep other com.android.internal.util as-is
+rule com.android.internal.util.** @0
 
 rule android.util.LocalLog* com.android.networkstack.tethering.util.LocalLog@1
 
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index 6488421..aac531a 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -103,6 +103,8 @@
 import com.android.net.module.util.NetworkStackConstants;
 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.networkstack.tethering.BpfCoordinator;
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
 import com.android.networkstack.tethering.PrivateAddressCoordinator;
@@ -112,8 +114,6 @@
 import com.android.networkstack.tethering.TetherDownstream6Key;
 import com.android.networkstack.tethering.TetherLimitKey;
 import com.android.networkstack.tethering.TetherLimitValue;
-import com.android.networkstack.tethering.TetherStatsKey;
-import com.android.networkstack.tethering.TetherStatsValue;
 import com.android.networkstack.tethering.TetherUpstream6Key;
 import com.android.networkstack.tethering.TetheringConfiguration;
 import com.android.networkstack.tethering.util.InterfaceSet;
@@ -156,6 +156,8 @@
     private static final int BLUETOOTH_DHCP_PREFIX_LENGTH = 24;
     private static final int DHCP_LEASE_TIME_SECS = 3600;
     private static final boolean DEFAULT_USING_BPF_OFFLOAD = true;
+    private static final int DEFAULT_SUBNET_PREFIX_LENGTH = 0;
+    private static final int P2P_SUBNET_PREFIX_LENGTH = 25;
 
     private static final InterfaceParams TEST_IFACE_PARAMS = new InterfaceParams(
             IFACE_NAME, 42 /* index */, MacAddress.ALL_ZEROS_ADDRESS, 1500 /* defaultMtu */);
@@ -230,6 +232,7 @@
 
         when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(usingBpfOffload);
         when(mTetherConfig.useLegacyDhcpServer()).thenReturn(usingLegacyDhcp);
+        when(mTetherConfig.getP2pLeasesSubnetPrefixLength()).thenReturn(P2P_SUBNET_PREFIX_LENGTH);
         mIpServer = new IpServer(
                 IFACE_NAME, mLooper.getLooper(), interfaceType, mSharedLog, mNetd, mBpfCoordinator,
                 mCallback, mTetherConfig, mAddressCoordinator, mDependencies);
@@ -1312,6 +1315,12 @@
         if (mIpServer.interfaceType() == TETHERING_NCM) {
             assertTrue(params.changePrefixOnDecline);
         }
+
+        if (mIpServer.interfaceType() == TETHERING_WIFI_P2P) {
+            assertEquals(P2P_SUBNET_PREFIX_LENGTH, params.leasesSubnetPrefixLength);
+        } else {
+            assertEquals(DEFAULT_SUBNET_PREFIX_LENGTH, params.leasesSubnetPrefixLength);
+        }
     }
 
     private void assertDhcpStarted(IpPrefix expectedPrefix) throws Exception {
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 4967d27..3630f24 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -102,6 +102,8 @@
 import com.android.net.module.util.NetworkStackConstants;
 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.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/TetheringConfigurationTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
index e8bb315..7fcf2b2 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
@@ -616,4 +616,35 @@
         assertArrayEquals(ncmRegexs, cfg.tetherableNcmRegexs);
     }
 
+    @Test
+    public void testP2pLeasesSubnetPrefixLength() throws Exception {
+        when(mResources.getBoolean(R.bool.config_tether_enable_legacy_wifi_p2p_dedicated_ip))
+                .thenReturn(true);
+
+        final int defaultSubnetPrefixLength = 0;
+        final TetheringConfiguration defaultCfg =
+                new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+        assertEquals(defaultSubnetPrefixLength, defaultCfg.getP2pLeasesSubnetPrefixLength());
+
+        final int prefixLengthTooSmall = -1;
+        when(mResources.getInteger(R.integer.config_p2p_leases_subnet_prefix_length)).thenReturn(
+                prefixLengthTooSmall);
+        final TetheringConfiguration tooSmallCfg =
+                new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+        assertEquals(defaultSubnetPrefixLength, tooSmallCfg.getP2pLeasesSubnetPrefixLength());
+
+        final int prefixLengthTooLarge = 31;
+        when(mResources.getInteger(R.integer.config_p2p_leases_subnet_prefix_length)).thenReturn(
+                prefixLengthTooLarge);
+        final TetheringConfiguration tooLargeCfg =
+                new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+        assertEquals(defaultSubnetPrefixLength, tooLargeCfg.getP2pLeasesSubnetPrefixLength());
+
+        final int p2pLeasesSubnetPrefixLength = 27;
+        when(mResources.getInteger(R.integer.config_p2p_leases_subnet_prefix_length)).thenReturn(
+                p2pLeasesSubnetPrefixLength);
+        final TetheringConfiguration p2pCfg =
+                new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+        assertEquals(p2pLeasesSubnetPrefixLength, p2pCfg.getP2pLeasesSubnetPrefixLength());
+    }
 }
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 0388758..2fd7f48 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -142,6 +142,7 @@
 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;
@@ -175,6 +176,7 @@
 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;
@@ -211,6 +213,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 import java.util.Vector;
 
 @RunWith(AndroidJUnit4.class)
@@ -1696,6 +1699,7 @@
         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.
@@ -1748,6 +1752,10 @@
             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);
@@ -1780,11 +1788,17 @@
             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());
         }
@@ -2836,53 +2850,81 @@
         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 */);
-        updateConfigAndVerifySupported(true /* supported */);
+        TestTetheringEventCallback callback = new TestTetheringEventCallback();
+        mTethering.registerTetheringEventCallback(callback);
+        mLooper.dispatchAll();
+        updateConfigAndVerifySupported(callback, expectedTypes);
 
         // Could disable tethering supported by settings.
         Settings.Global.putInt(mContentResolver, Settings.Global.TETHER_SUPPORTED, 0);
-        updateConfigAndVerifySupported(false /* supported */);
+        updateConfigAndVerifySupported(callback, new ArraySet<>());
 
         // Could disable tethering supported by user restriction.
         setTetheringSupported(true /* supported */);
+        updateConfigAndVerifySupported(callback, expectedTypes);
         when(mUserManager.hasUserRestriction(
                 UserManager.DISALLOW_CONFIG_TETHERING)).thenReturn(true);
-        updateConfigAndVerifySupported(false /* supported */);
+        updateConfigAndVerifySupported(callback, new ArraySet<>());
 
         // 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(true /* supported */);
+        updateConfigAndVerifySupported(callback, expectedTypes);
+        // Wifi tethering is not supported:
+        expectedTypes.remove(TETHERING_WIFI);
         when(mResources.getStringArray(R.array.config_tether_wifi_regexs))
                 .thenReturn(new String[0]);
-        updateConfigAndVerifySupported(true /* supported */);
-
+        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]);
 
         if (isAtLeastT()) {
-            when(mResources.getStringArray(R.array.config_tether_bluetooth_regexs))
-                    .thenReturn(new String[0]);
-            updateConfigAndVerifySupported(true /* supported */);
+            updateConfigAndVerifySupported(callback, expectedTypes);
+
+            // P2p tethering is not supported:
+            expectedTypes.remove(TETHERING_WIFI_P2P);
             when(mResources.getStringArray(R.array.config_tether_wifi_p2p_regexs))
                     .thenReturn(new String[0]);
-            updateConfigAndVerifySupported(true /* supported */);
+            updateConfigAndVerifySupported(callback, expectedTypes);
+            // Ncm tethering is not supported:
+            expectedTypes.remove(TETHERING_NCM);
             when(mResources.getStringArray(R.array.config_tether_ncm_regexs))
                     .thenReturn(new String[0]);
-            updateConfigAndVerifySupported(true /* supported */);
+            updateConfigAndVerifySupported(callback, expectedTypes);
+            // Ethernet tethering (last supported type) is not supported:
+            expectedTypes.remove(TETHERING_ETHERNET);
             mForceEthernetServiceUnavailable = true;
-            updateConfigAndVerifySupported(false /* supported */);
+            updateConfigAndVerifySupported(callback, new ArraySet<>());
+
         } else {
-            when(mResources.getStringArray(R.array.config_tether_bluetooth_regexs))
-                    .thenReturn(new String[0]);
-            updateConfigAndVerifySupported(false /* supported */);
+            // If wifi, usb and bluetooth are all not supported, all the types are not supported.
+            expectedTypes.clear();
+            updateConfigAndVerifySupported(callback, expectedTypes);
         }
     }
 
-    private void updateConfigAndVerifySupported(boolean supported) {
+    private void updateConfigAndVerifySupported(final TestTetheringEventCallback callback,
+            final ArraySet<Integer> expectedTypes) {
         sendConfigurationChanged();
-        assertEquals(supported, mTethering.isTetheringSupported());
+
+        assertEquals(expectedTypes.size() > 0, mTethering.isTetheringSupported());
+        callback.expectSupportedTetheringTypes(expectedTypes);
     }
     // TODO: Test that a request for hotspot mode doesn't interfere with an
     // already operating tethering mode interface.
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
index 6718402..1fe0e9a 100644
--- a/bpf_progs/Android.bp
+++ b/bpf_progs/Android.bp
@@ -47,6 +47,7 @@
         "//packages/modules/Connectivity/service/native/libs/libclat",
         "//packages/modules/Connectivity/Tethering",
         "//packages/modules/Connectivity/service/native",
+        "//packages/modules/Connectivity/tests/native",
         "//packages/modules/Connectivity/service-t/native/libs/libnetworkstats",
         "//packages/modules/Connectivity/tests/unit/jni",
         "//system/netd/server",
@@ -58,6 +59,16 @@
 // bpf kernel programs
 //
 bpf {
+    name: "block.o",
+    srcs: ["block.c"],
+    cflags: [
+        "-Wall",
+        "-Werror",
+    ],
+    sub_dir: "net_shared",
+}
+
+bpf {
     name: "dscp_policy.o",
     srcs: ["dscp_policy.c"],
     cflags: [
diff --git a/bpf_progs/block.c b/bpf_progs/block.c
new file mode 100644
index 0000000..ddd9a1c
--- /dev/null
+++ b/bpf_progs/block.c
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+
+#include <linux/types.h>
+#include <linux/bpf.h>
+#include <netinet/in.h>
+#include <stdint.h>
+
+#include "bpf_helpers.h"
+
+#define ALLOW 1
+#define DISALLOW 0
+
+DEFINE_BPF_MAP_GRW(blocked_ports_map, ARRAY, int, uint64_t,
+        1024 /* 64K ports -> 1024 u64s */, AID_SYSTEM)
+
+static inline __always_inline int block_port(struct bpf_sock_addr *ctx) {
+    if (!ctx->user_port) return ALLOW;
+
+    switch (ctx->protocol) {
+        case IPPROTO_TCP:
+        case IPPROTO_MPTCP:
+        case IPPROTO_UDP:
+        case IPPROTO_UDPLITE:
+        case IPPROTO_DCCP:
+        case IPPROTO_SCTP:
+            break;
+        default:
+            return ALLOW; // unknown protocols are allowed
+    }
+
+    int key = ctx->user_port >> 6;
+    int shift = ctx->user_port & 63;
+
+    uint64_t *val = bpf_blocked_ports_map_lookup_elem(&key);
+    // Lookup should never fail in reality, but if it does return here to keep the
+    // BPF verifier happy.
+    if (!val) return ALLOW;
+
+    if ((*val >> shift) & 1) return DISALLOW;
+    return ALLOW;
+}
+
+DEFINE_BPF_PROG_KVER("bind4/block_port", AID_ROOT, AID_SYSTEM,
+                     bind4_block_port, KVER(5, 4, 0))
+(struct bpf_sock_addr *ctx) {
+    return block_port(ctx);
+}
+
+DEFINE_BPF_PROG_KVER("bind6/block_port", AID_ROOT, AID_SYSTEM,
+                     bind6_block_port, KVER(5, 4, 0))
+(struct bpf_sock_addr *ctx) {
+    return block_port(ctx);
+}
+
+LICENSE("Apache 2.0");
+CRITICAL("ConnectivityNative");
diff --git a/bpf_progs/bpf_net_helpers.h b/bpf_progs/bpf_net_helpers.h
index c798580..e382713 100644
--- a/bpf_progs/bpf_net_helpers.h
+++ b/bpf_progs/bpf_net_helpers.h
@@ -65,8 +65,9 @@
            skb->pkt_type == PACKET_MULTICAST;
 }
 
-// try to make the first 'len' header bytes readable via direct packet access
-static inline __always_inline void try_make_readable(struct __sk_buff* skb, int len) {
+// try to make the first 'len' header bytes readable/writable via direct packet access
+// (note: AFAIK there is no way to ask for only direct packet read without also getting write)
+static inline __always_inline void try_make_writable(struct __sk_buff* skb, int len) {
     if (len > skb->len) len = skb->len;
     if (skb->data_end - skb->data < len) bpf_skb_pull_data(skb, len);
 }
diff --git a/bpf_progs/clatd.c b/bpf_progs/clatd.c
index dc646c3..9a9d337 100644
--- a/bpf_progs/clatd.c
+++ b/bpf_progs/clatd.c
@@ -37,21 +37,33 @@
 // From kernel:include/net/ip.h
 #define IP_DF 0x4000  // Flag: "Don't Fragment"
 
+// Used for iptables drops ingress clat packet. Beware of clat mark change may break the device
+// which is using the old clat mark in netd platform code. The reason is that the clat mark is a
+// mainline constant since T+ but netd iptable rules (ex: bandwidth control, firewall, and so on)
+// are set in stone.
+#define CLAT_MARK 0xdeadc1a7
+
 DEFINE_BPF_MAP_GRW(clat_ingress6_map, HASH, ClatIngress6Key, ClatIngress6Value, 16, AID_SYSTEM)
 
 static inline __always_inline int nat64(struct __sk_buff* skb, bool is_ethernet) {
-    const int l2_header_size = is_ethernet ? sizeof(struct ethhdr) : 0;
-    void* data = (void*)(long)skb->data;
-    const void* data_end = (void*)(long)skb->data_end;
-    const struct ethhdr* const eth = is_ethernet ? data : NULL;  // used iff is_ethernet
-    const struct ipv6hdr* const ip6 = is_ethernet ? (void*)(eth + 1) : data;
-
     // Require ethernet dst mac address to be our unicast address.
     if (is_ethernet && (skb->pkt_type != PACKET_HOST)) return TC_ACT_PIPE;
 
     // Must be meta-ethernet IPv6 frame
     if (skb->protocol != htons(ETH_P_IPV6)) return TC_ACT_PIPE;
 
+    const int l2_header_size = is_ethernet ? sizeof(struct ethhdr) : 0;
+
+    // Not clear if this is actually necessary considering we use DPA (Direct Packet Access),
+    // but we need to make sure we can read the IPv6 header reliably so that we can set
+    // skb->mark = 0xDeadC1a7 for packets we fail to offload.
+    try_make_writable(skb, l2_header_size + sizeof(struct ipv6hdr));
+
+    void* data = (void*)(long)skb->data;
+    const void* data_end = (void*)(long)skb->data_end;
+    const struct ethhdr* const eth = is_ethernet ? data : NULL;  // used iff is_ethernet
+    const struct ipv6hdr* const ip6 = is_ethernet ? (void*)(eth + 1) : data;
+
     // Must have (ethernet and) ipv6 header
     if (data + l2_header_size + sizeof(*ip6) > data_end) return TC_ACT_PIPE;
 
@@ -64,17 +76,6 @@
     // Maximum IPv6 payload length that can be translated to IPv4
     if (ntohs(ip6->payload_len) > 0xFFFF - sizeof(struct iphdr)) return TC_ACT_PIPE;
 
-    switch (ip6->nexthdr) {
-        case IPPROTO_TCP:  // For TCP & UDP the checksum neutrality of the chosen IPv6
-        case IPPROTO_UDP:  // address means there is no need to update their checksums.
-        case IPPROTO_GRE:  // We do not need to bother looking at GRE/ESP headers,
-        case IPPROTO_ESP:  // since there is never a checksum to update.
-            break;
-
-        default:  // do not know how to handle anything else
-            return TC_ACT_PIPE;
-    }
-
     ClatIngress6Key k = {
             .iif = skb->ifindex,
             .pfx96.in6_u.u6_addr32 =
@@ -90,6 +91,21 @@
 
     if (!v) return TC_ACT_PIPE;
 
+    switch (ip6->nexthdr) {
+        case IPPROTO_TCP:  // For TCP & UDP the checksum neutrality of the chosen IPv6
+        case IPPROTO_UDP:  // address means there is no need to update their checksums.
+        case IPPROTO_GRE:  // We do not need to bother looking at GRE/ESP headers,
+        case IPPROTO_ESP:  // since there is never a checksum to update.
+            break;
+
+        default:  // do not know how to handle anything else
+            // Mark ingress non-offloaded clat packet for dropping in ip6tables bw_raw_PREROUTING.
+            // Non-offloaded clat packet is going to be handled by clat daemon and ip6tables. The
+            // duplicate one in ip6tables is not necessary.
+            skb->mark = CLAT_MARK;
+            return TC_ACT_PIPE;
+    }
+
     struct ethhdr eth2;  // used iff is_ethernet
     if (is_ethernet) {
         eth2 = *eth;                     // Copy over the ethernet header (src/dst mac)
@@ -132,7 +148,13 @@
 
     // Packet mutations begin - point of no return, but if this first modification fails
     // the packet is probably still pristine, so let clatd handle it.
-    if (bpf_skb_change_proto(skb, htons(ETH_P_IP), 0)) return TC_ACT_PIPE;
+    if (bpf_skb_change_proto(skb, htons(ETH_P_IP), 0)) {
+        // Mark ingress non-offloaded clat packet for dropping in ip6tables bw_raw_PREROUTING.
+        // Non-offloaded clat packet is going to be handled by clat daemon and ip6tables. The
+        // duplicate one in ip6tables is not necessary.
+        skb->mark = CLAT_MARK;
+        return TC_ACT_PIPE;
+    }
 
     // This takes care of updating the skb->csum field for a CHECKSUM_COMPLETE packet.
     //
@@ -198,13 +220,16 @@
 
 DEFINE_BPF_PROG("schedcls/egress4/clat_rawip", AID_ROOT, AID_SYSTEM, sched_cls_egress4_clat_rawip)
 (struct __sk_buff* skb) {
+    // Must be meta-ethernet IPv4 frame
+    if (skb->protocol != htons(ETH_P_IP)) return TC_ACT_PIPE;
+
+    // Possibly not needed, but for consistency with nat64 up above
+    try_make_writable(skb, sizeof(struct iphdr));
+
     void* data = (void*)(long)skb->data;
     const void* data_end = (void*)(long)skb->data_end;
     const struct iphdr* const ip4 = data;
 
-    // Must be meta-ethernet IPv4 frame
-    if (skb->protocol != htons(ETH_P_IP)) return TC_ACT_PIPE;
-
     // Must have ipv4 header
     if (data + sizeof(*ip4) > data_end) return TC_ACT_PIPE;
 
diff --git a/bpf_progs/dscp_policy.c b/bpf_progs/dscp_policy.c
index 9989e6b..d5df7ef 100644
--- a/bpf_progs/dscp_policy.c
+++ b/bpf_progs/dscp_policy.c
@@ -16,6 +16,7 @@
 
 #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>
@@ -27,249 +28,294 @@
 #include <string.h>
 
 #include "bpf_helpers.h"
+#include "dscp_policy.h"
 
-#define MAX_POLICIES 16
-#define MAP_A 1
-#define MAP_B 2
-
-#define STRUCT_SIZE(name, size) _Static_assert(sizeof(name) == (size), "Incorrect struct size.")
-
-// TODO: these are already defined in /system/netd/bpf_progs/bpf_net_helpers.h
-// should they be moved to common location?
-static uint64_t (*bpf_get_socket_cookie)(struct __sk_buff* skb) =
-        (void*)BPF_FUNC_get_socket_cookie;
-static int (*bpf_skb_store_bytes)(struct __sk_buff* skb, __u32 offset, const void* from, __u32 len,
-                                  __u64 flags) = (void*)BPF_FUNC_skb_store_bytes;
-static int (*bpf_l3_csum_replace)(struct __sk_buff* skb, __u32 offset, __u64 from, __u64 to,
-                                  __u64 flags) = (void*)BPF_FUNC_l3_csum_replace;
-
-typedef struct {
-    // Add family here to match __sk_buff ?
-    struct in_addr srcIp;
-    struct in_addr dstIp;
-    __be16 srcPort;
-    __be16 dstPort;
-    uint8_t proto;
-    uint8_t dscpVal;
-    uint8_t pad[2];
-} Ipv4RuleEntry;
-STRUCT_SIZE(Ipv4RuleEntry, 2 * 4 + 2 * 2 + 2 * 1 + 2);  // 16, 4 for in_addr
-
-#define SRC_IP_MASK     1
-#define DST_IP_MASK     2
-#define SRC_PORT_MASK   4
-#define DST_PORT_MASK   8
-#define PROTO_MASK      16
-
-typedef struct {
-    struct in6_addr srcIp;
-    struct in6_addr dstIp;
-    __be16 srcPort;
-    __be16 dstPortStart;
-    __be16 dstPortEnd;
-    uint8_t proto;
-    uint8_t dscpVal;
-    uint8_t mask;
-    uint8_t pad[3];
-} Ipv4Policy;
-STRUCT_SIZE(Ipv4Policy, 2 * 16 + 3 * 2 + 3 * 1 + 3);  // 44
-
-typedef struct {
-    struct in6_addr srcIp;
-    struct in6_addr dstIp;
-    __be16 srcPort;
-    __be16 dstPortStart;
-    __be16 dstPortEnd;
-    uint8_t proto;
-    uint8_t dscpVal;
-    uint8_t mask;
-    // should we override this struct to include the param bitmask for linear search?
-    // For mapping socket to policies, all the params should match exactly since we can
-    // pull any missing from the sock itself.
-} Ipv6RuleEntry;
-STRUCT_SIZE(Ipv6RuleEntry, 2 * 16 + 3 * 2 + 3 * 1 + 3);  // 44
-
-// TODO: move to using 1 map. Map v4 address to 0xffff::v4
-DEFINE_BPF_MAP_GRW(ipv4_socket_to_policies_map_A, HASH, uint64_t, Ipv4RuleEntry, MAX_POLICIES,
-        AID_SYSTEM)
-DEFINE_BPF_MAP_GRW(ipv4_socket_to_policies_map_B, HASH, uint64_t, Ipv4RuleEntry, MAX_POLICIES,
-        AID_SYSTEM)
-DEFINE_BPF_MAP_GRW(ipv6_socket_to_policies_map_A, HASH, uint64_t, Ipv6RuleEntry, MAX_POLICIES,
-        AID_SYSTEM)
-DEFINE_BPF_MAP_GRW(ipv6_socket_to_policies_map_B, HASH, uint64_t, Ipv6RuleEntry, MAX_POLICIES,
-        AID_SYSTEM)
 DEFINE_BPF_MAP_GRW(switch_comp_map, ARRAY, int, uint64_t, 1, AID_SYSTEM)
 
-DEFINE_BPF_MAP_GRW(ipv4_dscp_policies_map, ARRAY, uint32_t, Ipv4Policy, MAX_POLICIES,
+DEFINE_BPF_MAP_GRW(ipv4_socket_to_policies_map_A, HASH, uint64_t, RuleEntry, MAX_POLICIES,
         AID_SYSTEM)
-DEFINE_BPF_MAP_GRW(ipv6_dscp_policies_map, ARRAY, uint32_t, Ipv6RuleEntry, MAX_POLICIES,
+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_PROG_KVER("schedcls/set_dscp", AID_ROOT, AID_SYSTEM,
-                     schedcls_set_dscp, KVER(5, 4, 0))
-(struct __sk_buff* skb) {
-    int one = 0;
-    uint64_t* selectedMap = bpf_switch_comp_map_lookup_elem(&one);
+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 TC_ACT_PIPE;
+        return;
     }
 
     // used for map lookup
     uint64_t cookie = bpf_get_socket_cookie(skb);
+    if (!cookie)
+        return;
 
-    // Do we need separate maps for ipv4/ipv6
-    if (skb->protocol == htons(ETH_P_IP)) { //maybe bpf_htons()
-        Ipv4RuleEntry* v4Policy;
-        if (*selectedMap == MAP_A) {
-            v4Policy = bpf_ipv4_socket_to_policies_map_A_lookup_elem(&cookie);
-        } else {
-            v4Policy = bpf_ipv4_socket_to_policies_map_B_lookup_elem(&cookie);
-        }
-
-        // How to use bitmask here to compare params efficiently?
-        // TODO: add BPF_PROG_TYPE_SK_SKB prog type to Loader?
-
-        void* data = (void*)(long)skb->data;
-        const void* data_end = (void*)(long)skb->data_end;
-        const struct iphdr* const iph = data;
-
+    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 + sizeof(*iph) > data_end) return TC_ACT_PIPE;
+        if (data + l2_header_size + sizeof(*iph) > data_end) return;
 
         // IP version must be 4
-        if (iph->version != 4) return TC_ACT_PIPE;
+        if (iph->version != 4) return;
 
         // We cannot handle IP options, just standard 20 byte == 5 dword minimal IPv4 header
-        if (iph->ihl != 5) return TC_ACT_PIPE;
+        if (iph->ihl != 5) return;
 
-        if (iph->protocol != IPPROTO_UDP) return TC_ACT_PIPE;
+        // V4 mapped address in in6_addr sets 10/11 position to 0xff.
+        srcIp.s6_addr32[2] = htonl(0x0000ffff);
+        dstIp.s6_addr32[2] = htonl(0x0000ffff);
 
-        struct udphdr *udp;
-        udp = data + sizeof(struct iphdr); //sizeof(struct ethhdr)
+        // 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 ((void*)(udp + 1) > data_end) return TC_ACT_PIPE;
+        if (ip6h->version != 6) return;
 
-        // Source/destination port in udphdr are stored in be16, need to convert to le16.
-        // This can be done via ntohs or htons. Is there a more preferred way?
-        // Cached policy was found.
-        if (v4Policy && iph->saddr == v4Policy->srcIp.s_addr &&
-                    iph->daddr == v4Policy->dstIp.s_addr &&
-                    ntohs(udp->source) == v4Policy->srcPort &&
-                    ntohs(udp->dest) == v4Policy->dstPort &&
-                    iph->protocol == v4Policy->proto) {
-            // set dscpVal in packet. Least sig 2 bits of TOS
-            // reference ipv4_change_dsfield()
+        srcIp = ip6h->saddr;
+        dstIp = ip6h->daddr;
+        protocol = ip6h->nexthdr;
+        priority = ip6h->priority;
+        flow_lbl = ip6h->flow_lbl[0];
+        hdr_size = sizeof(struct ipv6hdr);
+    }
 
-            // TODO: fix checksum...
-            int ecn = iph->tos & 3;
-            uint8_t newDscpVal = (v4Policy->dscpVal << 2) + ecn;
-            int oldDscpVal = iph->tos >> 2;
+    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);
-            return TC_ACT_PIPE;
+        } 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);
         }
 
-        // linear scan ipv4_dscp_policies_map, stored socket params do not match actual
-        int bestScore = -1;
-        uint32_t bestMatch = 0;
+        // 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;
 
-        for (register uint64_t i = 0; i < MAX_POLICIES; i++) {
-            int score = 0;
-            uint8_t tempMask = 0;
-            // Using a uint62 in for loop prevents infinite loop during BPF load,
-            // but the key is uint32, so convert back.
-            uint32_t key = i;
-            Ipv4Policy* policy = bpf_ipv4_dscp_policies_map_lookup_elem(&key);
+        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 mask is 0 continue, key does not have corresponding policy value
-            if (policy && policy->mask != 0) {
-                if ((policy->mask & SRC_IP_MASK) == SRC_IP_MASK &&
-                        iph->saddr == policy->srcIp.s6_addr32[3]) {
-                    score++;
-                    tempMask |= SRC_IP_MASK;
-                }
-                if ((policy->mask & DST_IP_MASK) == DST_IP_MASK &&
-                        iph->daddr == policy->dstIp.s6_addr32[3]) {
-                    score++;
-                    tempMask |= DST_IP_MASK;
-                }
-                if ((policy->mask & SRC_PORT_MASK) == SRC_PORT_MASK &&
-                        ntohs(udp->source) == htons(policy->srcPort)) {
-                    score++;
-                    tempMask |= SRC_PORT_MASK;
-                }
-                if ((policy->mask & DST_PORT_MASK) == DST_PORT_MASK &&
-                        ntohs(udp->dest) >= htons(policy->dstPortStart) &&
-                        ntohs(udp->dest) <= htons(policy->dstPortEnd)) {
-                    score++;
-                    tempMask |= DST_PORT_MASK;
-                }
-                if ((policy->mask & PROTO_MASK) == PROTO_MASK &&
-                        iph->protocol == policy->proto) {
-                    score++;
-                    tempMask |= PROTO_MASK;
-                }
+        if (score > bestScore && tempMask == policy->presentFields) {
+            bestMatch = i;
+            bestScore = score;
+        }
+    }
 
-                if (score > bestScore && tempMask == policy->mask) {
-                    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;
 
-        uint8_t newDscpVal = 0; // Can 0 be used as default forwarding value?
-        uint8_t curDscp = iph->tos & 252;
-        if (bestScore > 0) {
-            Ipv4Policy* policy = bpf_ipv4_dscp_policies_map_lookup_elem(&bestMatch);
-            if (policy) {
-                // TODO: if DSCP value is already set ignore?
-                // TODO: update checksum, for testing increment counter...
-                int ecn = iph->tos & 3;
-                newDscpVal = (policy->dscpVal << 2) + ecn;
-            }
-        }
+    RuleEntry value = {
+        .srcIp = srcIp,
+        .dstIp = dstIp,
+        .ifindex = skb->ifindex,
+        .srcPort = sport,
+        .dstPort = dport,
+        .proto = protocol,
+        .dscpVal = new_tos,
+    };
 
-        Ipv4RuleEntry value = {
-            .srcIp.s_addr = iph->saddr,
-            .dstIp.s_addr = iph->daddr,
-            .srcPort = udp->source,
-            .dstPort = udp->dest,
-            .proto = iph->protocol,
-            .dscpVal = newDscpVal,
-        };
-
-        if (!cookie)
-            return TC_ACT_PIPE;
-
-        // Update map
+    //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);
         }
-
-        // Need to store bytes after updating map or program will not load.
-        if (newDscpVal != curDscp) {
-            // 1 is the offset (Version/Header length)
-            int oldDscpVal = iph->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 if (skb->protocol == htons(ETH_P_IPV6)) { //maybe bpf_htons()
-        Ipv6RuleEntry* v6Policy;
+    } else {
         if (*selectedMap == MAP_A) {
-            v6Policy = bpf_ipv6_socket_to_policies_map_A_lookup_elem(&cookie);
+            bpf_ipv6_socket_to_policies_map_A_update_elem(&cookie, &value, BPF_ANY);
         } else {
-            v6Policy = bpf_ipv6_socket_to_policies_map_B_lookup_elem(&cookie);
+            bpf_ipv6_socket_to_policies_map_B_update_elem(&cookie, &value, BPF_ANY);
         }
+    }
 
-        if (!v6Policy)
-            return TC_ACT_PIPE;
+    // 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;
+}
 
-        // TODO: Add code to process IPv6 packet.
+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
diff --git a/bpf_progs/dscp_policy.h b/bpf_progs/dscp_policy.h
new file mode 100644
index 0000000..777c4ff
--- /dev/null
+++ b/bpf_progs/dscp_policy.h
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+
+#define MAX_POLICIES 16
+#define MAP_A 1
+#define MAP_B 2
+
+#define SRC_IP_MASK_FLAG     1
+#define DST_IP_MASK_FLAG     2
+#define SRC_PORT_MASK_FLAG   4
+#define DST_PORT_MASK_FLAG   8
+#define PROTO_MASK_FLAG      16
+
+#define STRUCT_SIZE(name, size) _Static_assert(sizeof(name) == (size), "Incorrect struct size.")
+
+#ifndef v6_equal
+#define v6_equal(a, b)    (a.s6_addr32[0] == b.s6_addr32[0] && \
+                 a.s6_addr32[1] == b.s6_addr32[1] && \
+                 a.s6_addr32[2] == b.s6_addr32[2] && \
+                 a.s6_addr32[3] == b.s6_addr32[3])
+#endif
+
+// TODO: these are already defined in packages/modules/Connectivity/bpf_progs/bpf_net_helpers.h.
+// smove to common location in future.
+static uint64_t (*bpf_get_socket_cookie)(struct __sk_buff* skb) =
+        (void*)BPF_FUNC_get_socket_cookie;
+static int (*bpf_skb_store_bytes)(struct __sk_buff* skb, __u32 offset, const void* from, __u32 len,
+                                  __u64 flags) = (void*)BPF_FUNC_skb_store_bytes;
+static int (*bpf_l3_csum_replace)(struct __sk_buff* skb, __u32 offset, __u64 from, __u64 to,
+                                  __u64 flags) = (void*)BPF_FUNC_l3_csum_replace;
+static long (*bpf_skb_ecn_set_ce)(struct __sk_buff* skb) =
+        (void*)BPF_FUNC_skb_ecn_set_ce;
+
+typedef struct {
+    struct in6_addr srcIp;
+    struct in6_addr dstIp;
+    uint32_t ifindex;
+    __be16 srcPort;
+    __be16 dstPortStart;
+    __be16 dstPortEnd;
+    uint8_t proto;
+    uint8_t dscpVal;
+    uint8_t presentFields;
+    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;
+    __u32 ifindex;
+    __be16 srcPort;
+    __be16 dstPort;
+    __u8 proto;
+    __u8 dscpVal;
+    __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/offload.c b/bpf_progs/offload.c
index 977e918..92a774c 100644
--- a/bpf_progs/offload.c
+++ b/bpf_progs/offload.c
@@ -122,7 +122,7 @@
     // not trigger and thus we need to manually make sure we can read packet headers via DPA.
     // Note: this is a blind best effort pull, which may fail or pull less - this doesn't matter.
     // It has to be done early cause it will invalidate any skb->data/data_end derived pointers.
-    try_make_readable(skb, l2_header_size + IP6_HLEN + TCP_HLEN);
+    try_make_writable(skb, l2_header_size + IP6_HLEN + TCP_HLEN);
 
     void* data = (void*)(long)skb->data;
     const void* data_end = (void*)(long)skb->data_end;
@@ -369,7 +369,7 @@
     // not trigger and thus we need to manually make sure we can read packet headers via DPA.
     // Note: this is a blind best effort pull, which may fail or pull less - this doesn't matter.
     // It has to be done early cause it will invalidate any skb->data/data_end derived pointers.
-    try_make_readable(skb, l2_header_size + IP4_HLEN + TCP_HLEN);
+    try_make_writable(skb, l2_header_size + IP4_HLEN + TCP_HLEN);
 
     void* data = (void*)(long)skb->data;
     const void* data_end = (void*)(long)skb->data_end;
diff --git a/framework-t/Sources.bp b/framework-t/Sources.bp
index 53b4163..b30ee80 100644
--- a/framework-t/Sources.bp
+++ b/framework-t/Sources.bp
@@ -124,13 +124,6 @@
     ],
 }
 
-// TODO: remove this empty filegroup.
-filegroup {
-    name: "framework-connectivity-tiramisu-sources",
-    srcs: [],
-    visibility: ["//frameworks/base"],
-}
-
 filegroup {
     name: "framework-connectivity-tiramisu-updatable-sources",
     srcs: [
diff --git a/framework-t/api/current.txt b/framework-t/api/current.txt
index 1b47481..eb77288 100644
--- a/framework-t/api/current.txt
+++ b/framework-t/api/current.txt
@@ -3,7 +3,7 @@
 
   public final class NetworkStats implements java.lang.AutoCloseable {
     method public void close();
-    method public boolean getNextBucket(android.app.usage.NetworkStats.Bucket);
+    method public boolean getNextBucket(@Nullable android.app.usage.NetworkStats.Bucket);
     method public boolean hasNextBucket();
   }
 
@@ -40,21 +40,21 @@
   }
 
   public class NetworkStatsManager {
-    method @WorkerThread public android.app.usage.NetworkStats queryDetails(int, String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
-    method @WorkerThread public android.app.usage.NetworkStats queryDetailsForUid(int, String, long, long, int) throws java.lang.SecurityException;
-    method @WorkerThread public android.app.usage.NetworkStats queryDetailsForUidTag(int, String, long, long, int, int) throws java.lang.SecurityException;
-    method @WorkerThread public android.app.usage.NetworkStats queryDetailsForUidTagState(int, String, long, long, int, int, int) throws java.lang.SecurityException;
-    method @WorkerThread public android.app.usage.NetworkStats querySummary(int, String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
-    method @WorkerThread public android.app.usage.NetworkStats.Bucket querySummaryForDevice(int, String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
-    method @WorkerThread public android.app.usage.NetworkStats.Bucket querySummaryForUser(int, String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
-    method public void registerUsageCallback(int, String, long, android.app.usage.NetworkStatsManager.UsageCallback);
-    method public void registerUsageCallback(int, String, long, android.app.usage.NetworkStatsManager.UsageCallback, @Nullable android.os.Handler);
-    method public void unregisterUsageCallback(android.app.usage.NetworkStatsManager.UsageCallback);
+    method @WorkerThread public android.app.usage.NetworkStats queryDetails(int, @Nullable String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
+    method @NonNull @WorkerThread public android.app.usage.NetworkStats queryDetailsForUid(int, @Nullable String, long, long, int) throws java.lang.SecurityException;
+    method @NonNull @WorkerThread public android.app.usage.NetworkStats queryDetailsForUidTag(int, @Nullable String, long, long, int, int) throws java.lang.SecurityException;
+    method @NonNull @WorkerThread public android.app.usage.NetworkStats queryDetailsForUidTagState(int, @Nullable String, long, long, int, int, int) throws java.lang.SecurityException;
+    method @WorkerThread public android.app.usage.NetworkStats querySummary(int, @Nullable String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
+    method @WorkerThread public android.app.usage.NetworkStats.Bucket querySummaryForDevice(int, @Nullable String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
+    method @WorkerThread public android.app.usage.NetworkStats.Bucket querySummaryForUser(int, @Nullable String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
+    method public void registerUsageCallback(int, @Nullable String, long, @NonNull android.app.usage.NetworkStatsManager.UsageCallback);
+    method public void registerUsageCallback(int, @Nullable String, long, @NonNull android.app.usage.NetworkStatsManager.UsageCallback, @Nullable android.os.Handler);
+    method public void unregisterUsageCallback(@NonNull android.app.usage.NetworkStatsManager.UsageCallback);
   }
 
   public abstract static class NetworkStatsManager.UsageCallback {
     ctor public NetworkStatsManager.UsageCallback();
-    method public abstract void onThresholdReached(int, String);
+    method public abstract void onThresholdReached(int, @Nullable String);
   }
 
 }
@@ -173,12 +173,12 @@
     method public static void incrementOperationCount(int, int);
     method public static void setThreadStatsTag(int);
     method public static void setThreadStatsUid(int);
-    method public static void tagDatagramSocket(java.net.DatagramSocket) throws java.net.SocketException;
-    method public static void tagFileDescriptor(java.io.FileDescriptor) throws java.io.IOException;
-    method public static void tagSocket(java.net.Socket) throws java.net.SocketException;
-    method public static void untagDatagramSocket(java.net.DatagramSocket) throws java.net.SocketException;
-    method public static void untagFileDescriptor(java.io.FileDescriptor) throws java.io.IOException;
-    method public static void untagSocket(java.net.Socket) throws java.net.SocketException;
+    method public static void tagDatagramSocket(@NonNull java.net.DatagramSocket) throws java.net.SocketException;
+    method public static void tagFileDescriptor(@NonNull java.io.FileDescriptor) throws java.io.IOException;
+    method public static void tagSocket(@NonNull java.net.Socket) throws java.net.SocketException;
+    method public static void untagDatagramSocket(@NonNull java.net.DatagramSocket) throws java.net.SocketException;
+    method public static void untagFileDescriptor(@NonNull java.io.FileDescriptor) throws java.io.IOException;
+    method public static void untagSocket(@NonNull java.net.Socket) throws java.net.SocketException;
     field public static final int UNSUPPORTED = -1; // 0xffffffff
   }
 
diff --git a/framework-t/api/lint-baseline.txt b/framework-t/api/lint-baseline.txt
index 53e1beb..2996a3e 100644
--- a/framework-t/api/lint-baseline.txt
+++ b/framework-t/api/lint-baseline.txt
@@ -41,86 +41,18 @@
     android.net.IpSecTransform.Builder does not declare a `build()` method, but builder classes are expected to
 
 
-MissingNullability: android.app.usage.NetworkStats#getNextBucket(android.app.usage.NetworkStats.Bucket) parameter #0:
-    Missing nullability on parameter `bucketOut` in method `getNextBucket`
 MissingNullability: android.app.usage.NetworkStatsManager#queryDetails(int, String, long, long):
     Missing nullability on method `queryDetails` return
-MissingNullability: android.app.usage.NetworkStatsManager#queryDetails(int, String, long, long) parameter #1:
-    Missing nullability on parameter `subscriberId` in method `queryDetails`
-MissingNullability: android.app.usage.NetworkStatsManager#queryDetailsForUid(int, String, long, long, int):
-    Missing nullability on method `queryDetailsForUid` return
-MissingNullability: android.app.usage.NetworkStatsManager#queryDetailsForUid(int, String, long, long, int) parameter #1:
-    Missing nullability on parameter `subscriberId` in method `queryDetailsForUid`
-MissingNullability: android.app.usage.NetworkStatsManager#queryDetailsForUidTag(int, String, long, long, int, int):
-    Missing nullability on method `queryDetailsForUidTag` return
-MissingNullability: android.app.usage.NetworkStatsManager#queryDetailsForUidTag(int, String, long, long, int, int) parameter #1:
-    Missing nullability on parameter `subscriberId` in method `queryDetailsForUidTag`
-MissingNullability: android.app.usage.NetworkStatsManager#queryDetailsForUidTagState(int, String, long, long, int, int, int):
-    Missing nullability on method `queryDetailsForUidTagState` return
-MissingNullability: android.app.usage.NetworkStatsManager#queryDetailsForUidTagState(int, String, long, long, int, int, int) parameter #1:
-    Missing nullability on parameter `subscriberId` in method `queryDetailsForUidTagState`
 MissingNullability: android.app.usage.NetworkStatsManager#querySummary(int, String, long, long):
     Missing nullability on method `querySummary` return
-MissingNullability: android.app.usage.NetworkStatsManager#querySummary(int, String, long, long) parameter #1:
-    Missing nullability on parameter `subscriberId` in method `querySummary`
 MissingNullability: android.app.usage.NetworkStatsManager#querySummaryForDevice(int, String, long, long):
     Missing nullability on method `querySummaryForDevice` return
-MissingNullability: android.app.usage.NetworkStatsManager#querySummaryForDevice(int, String, long, long) parameter #1:
-    Missing nullability on parameter `subscriberId` in method `querySummaryForDevice`
 MissingNullability: android.app.usage.NetworkStatsManager#querySummaryForUser(int, String, long, long):
     Missing nullability on method `querySummaryForUser` return
-MissingNullability: android.app.usage.NetworkStatsManager#querySummaryForUser(int, String, long, long) parameter #1:
-    Missing nullability on parameter `subscriberId` in method `querySummaryForUser`
-MissingNullability: android.app.usage.NetworkStatsManager#registerUsageCallback(int, String, long, android.app.usage.NetworkStatsManager.UsageCallback) parameter #1:
-    Missing nullability on parameter `subscriberId` in method `registerUsageCallback`
-MissingNullability: android.app.usage.NetworkStatsManager#registerUsageCallback(int, String, long, android.app.usage.NetworkStatsManager.UsageCallback) parameter #3:
-    Missing nullability on parameter `callback` in method `registerUsageCallback`
-MissingNullability: android.app.usage.NetworkStatsManager#registerUsageCallback(int, String, long, android.app.usage.NetworkStatsManager.UsageCallback, android.os.Handler) parameter #1:
-    Missing nullability on parameter `subscriberId` in method `registerUsageCallback`
-MissingNullability: android.app.usage.NetworkStatsManager#registerUsageCallback(int, String, long, android.app.usage.NetworkStatsManager.UsageCallback, android.os.Handler) parameter #3:
-    Missing nullability on parameter `callback` in method `registerUsageCallback`
-MissingNullability: android.app.usage.NetworkStatsManager#unregisterUsageCallback(android.app.usage.NetworkStatsManager.UsageCallback) parameter #0:
-    Missing nullability on parameter `callback` in method `unregisterUsageCallback`
-MissingNullability: android.app.usage.NetworkStatsManager.UsageCallback#onThresholdReached(int, String) parameter #1:
-    Missing nullability on parameter `subscriberId` in method `onThresholdReached`
 MissingNullability: android.net.IpSecAlgorithm#writeToParcel(android.os.Parcel, int) parameter #0:
     Missing nullability on parameter `out` in method `writeToParcel`
 MissingNullability: android.net.IpSecManager.UdpEncapsulationSocket#getFileDescriptor():
     Missing nullability on method `getFileDescriptor` return
-MissingNullability: android.net.TrafficStats#tagDatagramSocket(java.net.DatagramSocket) parameter #0:
-    Missing nullability on parameter `socket` in method `tagDatagramSocket`
-MissingNullability: android.net.TrafficStats#tagFileDescriptor(java.io.FileDescriptor) parameter #0:
-    Missing nullability on parameter `fd` in method `tagFileDescriptor`
-MissingNullability: android.net.TrafficStats#tagSocket(java.net.Socket) parameter #0:
-    Missing nullability on parameter `socket` in method `tagSocket`
-MissingNullability: android.net.TrafficStats#untagDatagramSocket(java.net.DatagramSocket) parameter #0:
-    Missing nullability on parameter `socket` in method `untagDatagramSocket`
-MissingNullability: android.net.TrafficStats#untagFileDescriptor(java.io.FileDescriptor) parameter #0:
-    Missing nullability on parameter `fd` in method `untagFileDescriptor`
-MissingNullability: android.net.TrafficStats#untagSocket(java.net.Socket) parameter #0:
-    Missing nullability on parameter `socket` in method `untagSocket`
-MissingNullability: com.android.internal.util.FileRotator#FileRotator(java.io.File, String, long, long) parameter #0:
-    Missing nullability on parameter `basePath` in method `FileRotator`
-MissingNullability: com.android.internal.util.FileRotator#FileRotator(java.io.File, String, long, long) parameter #1:
-    Missing nullability on parameter `prefix` in method `FileRotator`
-MissingNullability: com.android.internal.util.FileRotator#dumpAll(java.io.OutputStream) parameter #0:
-    Missing nullability on parameter `os` in method `dumpAll`
-MissingNullability: com.android.internal.util.FileRotator#readMatching(com.android.internal.util.FileRotator.Reader, long, long) parameter #0:
-    Missing nullability on parameter `reader` in method `readMatching`
-MissingNullability: com.android.internal.util.FileRotator#rewriteActive(com.android.internal.util.FileRotator.Rewriter, long) parameter #0:
-    Missing nullability on parameter `rewriter` in method `rewriteActive`
-MissingNullability: com.android.internal.util.FileRotator#rewriteAll(com.android.internal.util.FileRotator.Rewriter) parameter #0:
-    Missing nullability on parameter `rewriter` in method `rewriteAll`
-MissingNullability: com.android.internal.util.FileRotator.Reader#read(java.io.InputStream) parameter #0:
-    Missing nullability on parameter `in` in method `read`
-MissingNullability: com.android.internal.util.FileRotator.Writer#write(java.io.OutputStream) parameter #0:
-    Missing nullability on parameter `out` in method `write`
-MissingNullability: com.android.server.NetworkManagementSocketTagger#kernelToTag(String) parameter #0:
-    Missing nullability on parameter `string` in method `kernelToTag`
-MissingNullability: com.android.server.NetworkManagementSocketTagger#tag(java.io.FileDescriptor) parameter #0:
-    Missing nullability on parameter `fd` in method `tag`
-MissingNullability: com.android.server.NetworkManagementSocketTagger#untag(java.io.FileDescriptor) parameter #0:
-    Missing nullability on parameter `fd` in method `untag`
 
 
 RethrowRemoteException: android.app.usage.NetworkStatsManager#queryDetails(int, String, long, long):
diff --git a/framework-t/api/module-lib-current.txt b/framework-t/api/module-lib-current.txt
index 658c625..c1f7b39 100644
--- a/framework-t/api/module-lib-current.txt
+++ b/framework-t/api/module-lib-current.txt
@@ -4,6 +4,8 @@
   public class NetworkStatsManager {
     method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void forceUpdate();
     method public static int getCollapsedRatType(int);
+    method @NonNull @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public android.net.NetworkStats getMobileUidStats();
+    method @NonNull @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public android.net.NetworkStats getWifiUidStats();
     method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void noteUidForeground(int, boolean);
     method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void notifyNetworkStatus(@NonNull java.util.List<android.net.Network>, @NonNull java.util.List<android.net.NetworkStateSnapshot>, @Nullable String, @NonNull java.util.List<android.net.UnderlyingNetworkInfo>);
     method @NonNull @WorkerThread public android.app.usage.NetworkStats queryDetailsForDevice(@NonNull android.net.NetworkTemplate, long, long);
@@ -182,6 +184,7 @@
   public class TrafficStats {
     method public static void attachSocketTagger();
     method public static void init(@NonNull android.content.Context);
+    method public static void setThreadStatsTagDownload();
   }
 
   public final class UnderlyingNetworkInfo implements android.os.Parcelable {
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index 0f37b6f..6460fed 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -2,10 +2,8 @@
 package android.app.usage {
 
   public class NetworkStatsManager {
-    method @NonNull @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public android.net.NetworkStats getMobileUidStats();
-    method @NonNull @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public android.net.NetworkStats getWifiUidStats();
-    method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_STATS_PROVIDER, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void registerNetworkStatsProvider(@NonNull String, @NonNull android.net.netstats.provider.NetworkStatsProvider);
-    method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_STATS_PROVIDER, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void unregisterNetworkStatsProvider(@NonNull android.net.netstats.provider.NetworkStatsProvider);
+    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_STATS_PROVIDER, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void registerNetworkStatsProvider(@NonNull String, @NonNull android.net.netstats.provider.NetworkStatsProvider);
+    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_STATS_PROVIDER, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void unregisterNetworkStatsProvider(@NonNull android.net.netstats.provider.NetworkStatsProvider);
   }
 
 }
@@ -113,7 +111,6 @@
   public class TrafficStats {
     method public static void setThreadStatsTagApp();
     method public static void setThreadStatsTagBackup();
-    method public static void setThreadStatsTagDownload();
     method public static void setThreadStatsTagRestore();
     field public static final int TAG_NETWORK_STACK_IMPERSONATION_RANGE_END = -113; // 0xffffff8f
     field public static final int TAG_NETWORK_STACK_IMPERSONATION_RANGE_START = -128; // 0xffffff80
diff --git a/framework-t/src/android/app/usage/NetworkStats.java b/framework-t/src/android/app/usage/NetworkStats.java
index 2b6570a..74fe4bd 100644
--- a/framework-t/src/android/app/usage/NetworkStats.java
+++ b/framework-t/src/android/app/usage/NetworkStats.java
@@ -17,6 +17,7 @@
 package android.app.usage;
 
 import android.annotation.IntDef;
+import android.annotation.Nullable;
 import android.content.Context;
 import android.net.INetworkStatsService;
 import android.net.INetworkStatsSession;
@@ -474,10 +475,11 @@
 
     /**
      * Fills the recycled bucket with data of the next bin in the enumeration.
-     * @param bucketOut Bucket to be filled with data.
+     * @param bucketOut Bucket to be filled with data. If null, the method does
+     *                  nothing and returning false.
      * @return true if successfully filled the bucket, false otherwise.
      */
-    public boolean getNextBucket(Bucket bucketOut) {
+    public boolean getNextBucket(@Nullable Bucket bucketOut) {
         if (mSummary != null) {
             return getNextSummaryBucket(bucketOut);
         } else {
@@ -651,7 +653,7 @@
      * @param bucketOut Next item will be set here.
      * @return true if a next item could be set.
      */
-    private boolean getNextSummaryBucket(Bucket bucketOut) {
+    private boolean getNextSummaryBucket(@Nullable Bucket bucketOut) {
         if (bucketOut != null && mEnumerationIndex < mSummary.size()) {
             mRecycledSummaryEntry = mSummary.getValues(mEnumerationIndex++, mRecycledSummaryEntry);
             fillBucketFromSummaryEntry(bucketOut);
@@ -678,7 +680,7 @@
      * @param bucketOut Next item will be set here.
      * @return true if a next item could be set.
      */
-    private boolean getNextHistoryBucket(Bucket bucketOut) {
+    private boolean getNextHistoryBucket(@Nullable Bucket bucketOut) {
         if (bucketOut != null && mHistory != null) {
             if (mEnumerationIndex < mHistory.size()) {
                 mRecycledHistoryEntry = mHistory.getValues(mEnumerationIndex++,
diff --git a/framework-t/src/android/app/usage/NetworkStatsManager.java b/framework-t/src/android/app/usage/NetworkStatsManager.java
index bf518b2..f41475b 100644
--- a/framework-t/src/android/app/usage/NetworkStatsManager.java
+++ b/framework-t/src/android/app/usage/NetworkStatsManager.java
@@ -290,7 +290,7 @@
      *         statistics collection.
      */
     @WorkerThread
-    public Bucket querySummaryForDevice(int networkType, String subscriberId,
+    public Bucket querySummaryForDevice(int networkType, @Nullable String subscriberId,
             long startTime, long endTime) throws SecurityException, RemoteException {
         NetworkTemplate template;
         try {
@@ -335,8 +335,8 @@
      *         statistics collection.
      */
     @WorkerThread
-    public Bucket querySummaryForUser(int networkType, String subscriberId, long startTime,
-            long endTime) throws SecurityException, RemoteException {
+    public Bucket querySummaryForUser(int networkType, @Nullable String subscriberId,
+            long startTime, long endTime) throws SecurityException, RemoteException {
         NetworkTemplate template;
         try {
             template = createTemplate(networkType, subscriberId);
@@ -384,7 +384,7 @@
      *         statistics collection.
      */
     @WorkerThread
-    public NetworkStats querySummary(int networkType, String subscriberId, long startTime,
+    public NetworkStats querySummary(int networkType, @Nullable String subscriberId, long startTime,
             long endTime) throws SecurityException, RemoteException {
         NetworkTemplate template;
         try {
@@ -508,15 +508,17 @@
      *
      * @see #queryDetailsForUidTagState(int, String, long, long, int, int, int)
      */
+    @NonNull
     @WorkerThread
-    public NetworkStats queryDetailsForUid(int networkType, String subscriberId,
+    public NetworkStats queryDetailsForUid(int networkType, @Nullable String subscriberId,
             long startTime, long endTime, int uid) throws SecurityException {
         return queryDetailsForUidTagState(networkType, subscriberId, startTime, endTime, uid,
             NetworkStats.Bucket.TAG_NONE, NetworkStats.Bucket.STATE_ALL);
     }
 
     /** @hide */
-    public NetworkStats queryDetailsForUid(NetworkTemplate template,
+    @NonNull
+    public NetworkStats queryDetailsForUid(@NonNull NetworkTemplate template,
             long startTime, long endTime, int uid) throws SecurityException {
         return queryDetailsForUidTagState(template, startTime, endTime, uid,
                 NetworkStats.Bucket.TAG_NONE, NetworkStats.Bucket.STATE_ALL);
@@ -524,23 +526,59 @@
 
     /**
      * Query network usage statistics details for a given uid and tag.
+     *
+     * This may take a long time, and apps should avoid calling this on their main thread.
+     * Only usable for uids belonging to calling user. Result is not aggregated over time.
+     * This means buckets' start and end timestamps are going to be between 'startTime' and
+     * 'endTime' parameters. The uid is going to be the same as the 'uid' parameter, the tag
+     * the same as the 'tag' parameter, and the state the same as the 'state' parameter.
+     * defaultNetwork is going to be {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL},
+     * metered is going to be {@link NetworkStats.Bucket#METERED_ALL}, and
+     * roaming is going to be {@link NetworkStats.Bucket#ROAMING_ALL}.
+     * <p>Only includes buckets that atomically occur in the inclusive time range. Doesn't
+     * interpolate across partial buckets. Since bucket length is in the order of hours, this
+     * method cannot be used to measure data usage on a fine grained time scale.
      * This may take a long time, and apps should avoid calling this on their main thread.
      *
-     * @see #queryDetailsForUidTagState(int, String, long, long, int, int, int)
+     * @param networkType As defined in {@link ConnectivityManager}, e.g.
+     *            {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI}
+     *            etc.
+     * @param subscriberId If applicable, the subscriber id of the network interface.
+     *                     <p>Starting with API level 29, the {@code subscriberId} is guarded by
+     *                     additional restrictions. Calling apps that do not meet the new
+     *                     requirements to access the {@code subscriberId} can provide a {@code
+     *                     null} value when querying for the mobile network type to receive usage
+     *                     for all mobile networks. For additional details see {@link
+     *                     TelephonyManager#getSubscriberId()}.
+     *                     <p>Starting with API level 31, calling apps can provide a
+     *                     {@code subscriberId} with wifi network type to receive usage for
+     *                     wifi networks which is under the given subscription if applicable.
+     *                     Otherwise, pass {@code null} when querying all wifi networks.
+     * @param startTime Start of period. Defined in terms of "Unix time", see
+     *            {@link java.lang.System#currentTimeMillis}.
+     * @param endTime End of period. Defined in terms of "Unix time", see
+     *            {@link java.lang.System#currentTimeMillis}.
+     * @param uid UID of app
+     * @param tag TAG of interest. Use {@link NetworkStats.Bucket#TAG_NONE} for aggregated data
+     *            across all the tags.
+     * @return Statistics which is described above.
+     * @throws SecurityException if permissions are insufficient to read network statistics.
      */
+    @NonNull
     @WorkerThread
-    public NetworkStats queryDetailsForUidTag(int networkType, String subscriberId,
+    public NetworkStats queryDetailsForUidTag(int networkType, @Nullable String subscriberId,
             long startTime, long endTime, int uid, int tag) throws SecurityException {
         return queryDetailsForUidTagState(networkType, subscriberId, startTime, endTime, uid,
             tag, NetworkStats.Bucket.STATE_ALL);
     }
 
     /**
-     * Query network usage statistics details for a given uid, tag, and state. Only usable for uids
-     * belonging to calling user. Result is not aggregated over time. This means buckets' start and
-     * end timestamps are going to be between 'startTime' and 'endTime' parameters. The uid is going
-     * to be the same as the 'uid' parameter, the tag the same as the 'tag' parameter, and the state
-     * the same as the 'state' parameter.
+     * Query network usage statistics details for a given uid, tag, and state.
+     *
+     * Only usable for uids belonging to calling user. Result is not aggregated over time.
+     * This means buckets' start and end timestamps are going to be between 'startTime' and
+     * 'endTime' parameters. The uid is going to be the same as the 'uid' parameter, the tag
+     * the same as the 'tag' parameter, and the state the same as the 'state' parameter.
      * defaultNetwork is going to be {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL},
      * metered is going to be {@link NetworkStats.Bucket#METERED_ALL}, and
      * roaming is going to be {@link NetworkStats.Bucket#ROAMING_ALL}.
@@ -572,11 +610,12 @@
      *            across all the tags.
      * @param state state of interest. Use {@link NetworkStats.Bucket#STATE_ALL} to aggregate
      *            traffic from all states.
-     * @return Statistics object or null if an error happened during statistics collection.
+     * @return Statistics which is described above.
      * @throws SecurityException if permissions are insufficient to read network statistics.
      */
+    @NonNull
     @WorkerThread
-    public NetworkStats queryDetailsForUidTagState(int networkType, String subscriberId,
+    public NetworkStats queryDetailsForUidTagState(int networkType, @Nullable String subscriberId,
             long startTime, long endTime, int uid, int tag, int state) throws SecurityException {
         NetworkTemplate template;
         template = createTemplate(networkType, subscriberId);
@@ -669,7 +708,7 @@
      *         statistics collection.
      */
     @WorkerThread
-    public NetworkStats queryDetails(int networkType, String subscriberId, long startTime,
+    public NetworkStats queryDetails(int networkType, @Nullable String subscriberId, long startTime,
             long endTime) throws SecurityException, RemoteException {
         NetworkTemplate template;
         try {
@@ -698,7 +737,7 @@
      *
      * @hide
      */
-    @SystemApi
+    @SystemApi(client = MODULE_LIBRARIES)
     @RequiresPermission(anyOf = {
             NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
             android.Manifest.permission.NETWORK_STACK})
@@ -724,7 +763,7 @@
      *
      * @hide
      */
-    @SystemApi
+    @SystemApi(client = MODULE_LIBRARIES)
     @RequiresPermission(anyOf = {
             NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
             android.Manifest.permission.NETWORK_STACK})
@@ -785,10 +824,28 @@
     /**
      * Registers to receive notifications about data usage on specified networks.
      *
-     * @see #registerUsageCallback(int, String, long, UsageCallback, Handler)
+     * <p>The callbacks will continue to be called as long as the process is live or
+     * {@link #unregisterUsageCallback} is called.
+     *
+     * @param networkType Type of network to monitor. Either
+    {@link ConnectivityManager#TYPE_MOBILE} or {@link ConnectivityManager#TYPE_WIFI}.
+     * @param subscriberId If applicable, the subscriber id of the network interface.
+     *                     <p>Starting with API level 29, the {@code subscriberId} is guarded by
+     *                     additional restrictions. Calling apps that do not meet the new
+     *                     requirements to access the {@code subscriberId} can provide a {@code
+     *                     null} value when registering for the mobile network type to receive
+     *                     notifications for all mobile networks. For additional details see {@link
+     *                     TelephonyManager#getSubscriberId()}.
+     *                     <p>Starting with API level 31, calling apps can provide a
+     *                     {@code subscriberId} with wifi network type to receive usage for
+     *                     wifi networks which is under the given subscription if applicable.
+     *                     Otherwise, pass {@code null} when querying all wifi networks.
+     * @param thresholdBytes Threshold in bytes to be notified on.
+     * @param callback The {@link UsageCallback} that the system will call when data usage
+     *            has exceeded the specified threshold.
      */
-    public void registerUsageCallback(int networkType, String subscriberId, long thresholdBytes,
-            UsageCallback callback) {
+    public void registerUsageCallback(int networkType, @Nullable String subscriberId,
+            long thresholdBytes, @NonNull UsageCallback callback) {
         registerUsageCallback(networkType, subscriberId, thresholdBytes, callback,
                 null /* handler */);
     }
@@ -818,8 +875,8 @@
      * @param handler to dispatch callback events through, otherwise if {@code null} it uses
      *            the calling thread.
      */
-    public void registerUsageCallback(int networkType, String subscriberId, long thresholdBytes,
-            UsageCallback callback, @Nullable Handler handler) {
+    public void registerUsageCallback(int networkType, @Nullable String subscriberId,
+            long thresholdBytes, @NonNull UsageCallback callback, @Nullable Handler handler) {
         NetworkTemplate template = createTemplate(networkType, subscriberId);
         if (DBG) {
             Log.d(TAG, "registerUsageCallback called with: {"
@@ -839,7 +896,7 @@
      *
      * @param callback The {@link UsageCallback} used when registering.
      */
-    public void unregisterUsageCallback(UsageCallback callback) {
+    public void unregisterUsageCallback(@NonNull UsageCallback callback) {
         if (callback == null || callback.request == null
                 || callback.request.requestId == DataUsageRequest.REQUEST_ID_UNSET) {
             throw new IllegalArgumentException("Invalid UsageCallback");
@@ -880,7 +937,7 @@
         /**
          * Called when data usage has reached the given threshold.
          */
-        public abstract void onThresholdReached(int networkType, String subscriberId);
+        public abstract void onThresholdReached(int networkType, @Nullable String subscriberId);
 
         /**
          * @hide used for internal bookkeeping
@@ -924,7 +981,7 @@
     @RequiresPermission(anyOf = {
             android.Manifest.permission.NETWORK_STATS_PROVIDER,
             NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK})
-    @NonNull public void registerNetworkStatsProvider(
+    public void registerNetworkStatsProvider(
             @NonNull String tag,
             @NonNull NetworkStatsProvider provider) {
         try {
@@ -950,7 +1007,7 @@
     @RequiresPermission(anyOf = {
             android.Manifest.permission.NETWORK_STATS_PROVIDER,
             NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK})
-    @NonNull public void unregisterNetworkStatsProvider(@NonNull NetworkStatsProvider provider) {
+    public void unregisterNetworkStatsProvider(@NonNull NetworkStatsProvider provider) {
         try {
             provider.getProviderCallbackBinderOrThrow().unregister();
         } catch (RemoteException e) {
@@ -958,7 +1015,7 @@
         }
     }
 
-    private static NetworkTemplate createTemplate(int networkType, String subscriberId) {
+    private static NetworkTemplate createTemplate(int networkType, @Nullable String subscriberId) {
         final NetworkTemplate template;
         switch (networkType) {
             case ConnectivityManager.TYPE_MOBILE:
diff --git a/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java b/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
index 61b34d0..d9c9d74 100644
--- a/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
+++ b/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
@@ -20,7 +20,9 @@
 import android.app.SystemServiceRegistry;
 import android.app.usage.NetworkStatsManager;
 import android.content.Context;
+import android.net.mdns.aidl.IMDns;
 import android.net.nsd.INsdManager;
+import android.net.nsd.MDnsManager;
 import android.net.nsd.NsdManager;
 
 /**
@@ -78,5 +80,14 @@
                     return new EthernetManager(context, service);
                 }
         );
+
+        SystemServiceRegistry.registerStaticService(
+                MDnsManager.MDNS_SERVICE,
+                MDnsManager.class,
+                (serviceBinder) -> {
+                    IMDns service = IMDns.Stub.asInterface(serviceBinder);
+                    return new MDnsManager(service);
+                }
+        );
     }
 }
diff --git a/framework-t/src/android/net/EthernetManager.java b/framework-t/src/android/net/EthernetManager.java
index e02ea89..2b76dd9 100644
--- a/framework-t/src/android/net/EthernetManager.java
+++ b/framework-t/src/android/net/EthernetManager.java
@@ -541,8 +541,7 @@
      * Similarly, use {@link NetworkCapabilities.Builder} to build a {@code NetworkCapabilities}
      * object for this network to put inside the {@code request}.
      *
-     * This function accepts an {@link OutcomeReceiver} that is called once the operation has
-     * finished execution.
+     * The provided {@link OutcomeReceiver} is called once the operation has finished execution.
      *
      * @param iface the name of the interface to act upon.
      * @param request the {@link EthernetNetworkUpdateRequest} used to set an ethernet network's
@@ -554,7 +553,8 @@
      *                 information about the error.
      * @throws SecurityException if the process doesn't hold
      *                          {@link android.Manifest.permission.MANAGE_ETHERNET_NETWORKS}.
-     * @throws UnsupportedOperationException if called on a non-automotive device or on an
+     * @throws UnsupportedOperationException if the {@link NetworkCapabilities} are updated on a
+     *                                       non-automotive device or this function is called on an
      *                                       unsupported interface.
      * @hide
      */
@@ -582,9 +582,9 @@
     /**
      * Enable a network interface.
      *
-     * Enables a previously disabled network interface.
-     * This function accepts an {@link OutcomeReceiver} that is called once the operation has
-     * finished execution.
+     * Enables a previously disabled network interface. An attempt to enable an already-enabled
+     * interface is ignored.
+     * The provided {@link OutcomeReceiver} is called once the operation has finished execution.
      *
      * @param iface the name of the interface to enable.
      * @param executor an {@link Executor} to execute the callback on. Optional if callback is null.
@@ -619,10 +619,9 @@
     /**
      * Disable a network interface.
      *
-     * Disables the use of a network interface to fulfill network requests. If the interface
-     * currently serves a request, the network will be torn down.
-     * This function accepts an {@link OutcomeReceiver} that is called once the operation has
-     * finished execution.
+     * Disables the specified interface. If this interface is in use in a connected
+     * {@link android.net.Network}, then that {@code Network} will be torn down.
+     * The provided {@link OutcomeReceiver} is called once the operation has finished execution.
      *
      * @param iface the name of the interface to disable.
      * @param executor an {@link Executor} to execute the callback on. Optional if callback is null.
diff --git a/framework-t/src/android/net/NetworkIdentitySet.java b/framework-t/src/android/net/NetworkIdentitySet.java
index ad3a958..d88408e 100644
--- a/framework-t/src/android/net/NetworkIdentitySet.java
+++ b/framework-t/src/android/net/NetworkIdentitySet.java
@@ -206,6 +206,7 @@
     public static int compare(@NonNull NetworkIdentitySet left, @NonNull NetworkIdentitySet right) {
         Objects.requireNonNull(left);
         Objects.requireNonNull(right);
+        if (left.isEmpty() && right.isEmpty()) return 0;
         if (left.isEmpty()) return -1;
         if (right.isEmpty()) return 1;
 
diff --git a/framework-t/src/android/net/NetworkStats.java b/framework-t/src/android/net/NetworkStats.java
index 06f2a62..51ff5ec 100644
--- a/framework-t/src/android/net/NetworkStats.java
+++ b/framework-t/src/android/net/NetworkStats.java
@@ -124,7 +124,6 @@
     public @Nullable static final String[] INTERFACES_ALL = null;
 
     /** {@link #tag} value for total data across all tags. */
-    // TODO: Rename TAG_NONE to TAG_ALL.
     public static final int TAG_NONE = 0;
 
     /** {@link #metered} value to account for all metered states. */
@@ -412,21 +411,24 @@
         /**
          * @return the metered state.
          */
-        @Meteredness public int getMetered() {
+        @Meteredness
+        public int getMetered() {
             return metered;
         }
 
         /**
          * @return the roaming state.
          */
-        @Roaming public int getRoaming() {
+        @Roaming
+        public int getRoaming() {
             return roaming;
         }
 
         /**
          * @return the default network state.
          */
-        @DefaultNetwork public int getDefaultNetwork() {
+        @DefaultNetwork
+        public int getDefaultNetwork() {
             return defaultNetwork;
         }
 
diff --git a/framework-t/src/android/net/NetworkStatsCollection.java b/framework-t/src/android/net/NetworkStatsCollection.java
index e385b33..b59a890 100644
--- a/framework-t/src/android/net/NetworkStatsCollection.java
+++ b/framework-t/src/android/net/NetworkStatsCollection.java
@@ -776,7 +776,7 @@
             if (!templateMatches(groupTemplate, key.ident)) continue;
             if (key.set >= NetworkStats.SET_DEBUG_START) continue;
 
-            final Key groupKey = new Key(null, key.uid, key.set, key.tag);
+            final Key groupKey = new Key(new NetworkIdentitySet(), key.uid, key.set, key.tag);
             NetworkStatsHistory groupHistory = grouped.get(groupKey);
             if (groupHistory == null) {
                 groupHistory = new NetworkStatsHistory(value.getBucketDuration());
diff --git a/framework-t/src/android/net/NetworkTemplate.java b/framework-t/src/android/net/NetworkTemplate.java
index 7b5afd7..b82a126 100644
--- a/framework-t/src/android/net/NetworkTemplate.java
+++ b/framework-t/src/android/net/NetworkTemplate.java
@@ -393,8 +393,9 @@
         //constructor passes METERED_YES for these types.
         this(matchRule, subscriberId, matchSubscriberIds,
                 wifiNetworkKey != null ? new String[] { wifiNetworkKey } : new String[0],
-                (matchRule == MATCH_MOBILE || matchRule == MATCH_MOBILE_WILDCARD) ? METERED_YES
-                : METERED_ALL , ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+                (matchRule == MATCH_MOBILE || matchRule == MATCH_MOBILE_WILDCARD
+                        || matchRule == MATCH_CARRIER) ? METERED_YES : METERED_ALL,
+                ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
                 OEM_MANAGED_ALL, NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT);
     }
 
diff --git a/framework-t/src/android/net/TrafficStats.java b/framework-t/src/android/net/TrafficStats.java
index bc836d8..dc4ac55 100644
--- a/framework-t/src/android/net/TrafficStats.java
+++ b/framework-t/src/android/net/TrafficStats.java
@@ -205,7 +205,7 @@
      *                server context.
      * @hide
      */
-    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    @SystemApi(client = MODULE_LIBRARIES)
     @SuppressLint("VisiblySynchronized")
     public static synchronized void init(@NonNull final Context context) {
         if (sStatsService != null) {
@@ -376,7 +376,7 @@
      *
      * @hide
      */
-    @SystemApi
+    @SystemApi(client = MODULE_LIBRARIES)
     public static void setThreadStatsTagDownload() {
         setThreadStatsTag(TAG_SYSTEM_DOWNLOAD);
     }
@@ -468,7 +468,7 @@
      *
      * @see #setThreadStatsTag(int)
      */
-    public static void tagSocket(Socket socket) throws SocketException {
+    public static void tagSocket(@NonNull Socket socket) throws SocketException {
         SocketTagger.get().tag(socket);
     }
 
@@ -483,7 +483,7 @@
      * calling {@code untagSocket()} before sending the socket to another
      * process.
      */
-    public static void untagSocket(Socket socket) throws SocketException {
+    public static void untagSocket(@NonNull Socket socket) throws SocketException {
         SocketTagger.get().untag(socket);
     }
 
@@ -496,14 +496,14 @@
      *
      * @see #setThreadStatsTag(int)
      */
-    public static void tagDatagramSocket(DatagramSocket socket) throws SocketException {
+    public static void tagDatagramSocket(@NonNull DatagramSocket socket) throws SocketException {
         SocketTagger.get().tag(socket);
     }
 
     /**
      * Remove any statistics parameters from the given {@link DatagramSocket}.
      */
-    public static void untagDatagramSocket(DatagramSocket socket) throws SocketException {
+    public static void untagDatagramSocket(@NonNull DatagramSocket socket) throws SocketException {
         SocketTagger.get().untag(socket);
     }
 
@@ -516,7 +516,7 @@
      *
      * @see #setThreadStatsTag(int)
      */
-    public static void tagFileDescriptor(FileDescriptor fd) throws IOException {
+    public static void tagFileDescriptor(@NonNull FileDescriptor fd) throws IOException {
         SocketTagger.get().tag(fd);
     }
 
@@ -524,7 +524,7 @@
      * Remove any statistics parameters from the given {@link FileDescriptor}
      * socket.
      */
-    public static void untagFileDescriptor(FileDescriptor fd) throws IOException {
+    public static void untagFileDescriptor(@NonNull FileDescriptor fd) throws IOException {
         SocketTagger.get().untag(fd);
     }
 
diff --git a/framework-t/src/android/net/nsd/MDnsManager.java b/framework-t/src/android/net/nsd/MDnsManager.java
new file mode 100644
index 0000000..c11e60c
--- /dev/null
+++ b/framework-t/src/android/net/nsd/MDnsManager.java
@@ -0,0 +1,200 @@
+/*
+ * 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 android.net.nsd;
+
+import android.annotation.NonNull;
+import android.net.mdns.aidl.DiscoveryInfo;
+import android.net.mdns.aidl.GetAddressInfo;
+import android.net.mdns.aidl.IMDns;
+import android.net.mdns.aidl.IMDnsEventListener;
+import android.net.mdns.aidl.RegistrationInfo;
+import android.net.mdns.aidl.ResolutionInfo;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.util.Log;
+
+/**
+ * A manager class for mdns service.
+ *
+ * @hide
+ */
+public class MDnsManager {
+    private static final String TAG = MDnsManager.class.getSimpleName();
+    private final IMDns mMdns;
+
+    /** Service name for this. */
+    public static final String MDNS_SERVICE = "mdns";
+
+    private static final int NO_RESULT = -1;
+    private static final int NETID_UNSET = 0;
+
+    public MDnsManager(IMDns mdns) {
+        mMdns = mdns;
+    }
+
+    /**
+     * Start the MDNSResponder daemon.
+     */
+    public void startDaemon() {
+        try {
+            mMdns.startDaemon();
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "Start mdns failed.", e);
+        }
+    }
+
+    /**
+     * Stop the MDNSResponder daemon.
+     */
+    public void stopDaemon() {
+        try {
+            mMdns.stopDaemon();
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "Stop mdns failed.", e);
+        }
+    }
+
+    /**
+     * Start registering a service.
+     *
+     * @param id The operation ID.
+     * @param serviceName The service name to be registered.
+     * @param registrationType The service type to be registered.
+     * @param port The port on which the service accepts connections.
+     * @param txtRecord The txt record. Refer to {@code NsdServiceInfo#setTxtRecords} for details.
+     * @param interfaceIdx The interface index on which to register the service.
+     * @return {@code true} if registration is successful, else {@code false}.
+     */
+    public boolean registerService(int id, @NonNull String serviceName,
+            @NonNull String registrationType, int port, @NonNull byte[] txtRecord,
+            int interfaceIdx) {
+        final RegistrationInfo info = new RegistrationInfo(id, NO_RESULT, serviceName,
+                registrationType, port, txtRecord, interfaceIdx);
+        try {
+            mMdns.registerService(info);
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "Register service failed.", e);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Start discovering services.
+     *
+     * @param id The operation ID.
+     * @param registrationType The service type to be discovered.
+     * @param interfaceIdx The interface index on which to discover for services.
+     * @return {@code true} if discovery is started successfully, else {@code false}.
+     */
+    public boolean discover(int id, @NonNull String registrationType, int interfaceIdx) {
+        final DiscoveryInfo info = new DiscoveryInfo(id, NO_RESULT, "" /* serviceName */,
+                registrationType, "" /* domainName */, interfaceIdx, NETID_UNSET);
+        try {
+            mMdns.discover(info);
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "Discover service failed.", e);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Start resolving the target service.
+     *
+     * @param id The operation ID.
+     * @param serviceName The service name to be resolved.
+     * @param registrationType The service type to be resolved.
+     * @param domain The service domain to be resolved.
+     * @param interfaceIdx The interface index on which to resolve the service.
+     * @return {@code true} if resolution is started successfully, else {@code false}.
+     */
+    public boolean resolve(int id, @NonNull String serviceName, @NonNull String registrationType,
+            @NonNull String domain, int interfaceIdx) {
+        final ResolutionInfo info = new ResolutionInfo(id, NO_RESULT, serviceName,
+                registrationType, domain, "" /* serviceFullName */, "" /* hostname */, 0 /* port */,
+                new byte[0] /* txtRecord */, interfaceIdx);
+        try {
+            mMdns.resolve(info);
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "Resolve service failed.", e);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Start getting the target service address.
+     *
+     * @param id The operation ID.
+     * @param hostname The fully qualified domain name of the host to be queried for.
+     * @param interfaceIdx The interface index on which to issue the query.
+     * @return {@code true} if getting address is started successful, else {@code false}.
+     */
+    public boolean getServiceAddress(int id, @NonNull String hostname, int interfaceIdx) {
+        final GetAddressInfo info = new GetAddressInfo(id, NO_RESULT, hostname,
+                "" /* address */, interfaceIdx, NETID_UNSET);
+        try {
+            mMdns.getServiceAddress(info);
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "Get service address failed.", e);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Stop an operation which was requested before.
+     *
+     * @param id the operation id to be stopped.
+     * @return {@code true} if operation is stopped successfully, else {@code false}.
+     */
+    public boolean stopOperation(int id) {
+        try {
+            mMdns.stopOperation(id);
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "Stop operation failed.", e);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Register an event listener.
+     *
+     * @param listener The listener to be registered.
+     */
+    public void registerEventListener(@NonNull IMDnsEventListener listener) {
+        try {
+            mMdns.registerEventListener(listener);
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "Register listener failed.", e);
+        }
+    }
+
+    /**
+     * Unregister an event listener.
+     *
+     * @param listener The listener to be unregistered.
+     */
+    public void unregisterEventListener(@NonNull IMDnsEventListener listener) {
+        try {
+            mMdns.unregisterEventListener(listener);
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "Unregister listener failed.", e);
+        }
+    }
+}
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index 209f372..33b44c8 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -235,7 +235,7 @@
     public static final int DISABLE                                 = 21;
 
     /** @hide */
-    public static final int NATIVE_DAEMON_EVENT                     = 22;
+    public static final int MDNS_SERVICE_EVENT                      = 22;
 
     /** @hide */
     public static final int REGISTER_CLIENT                         = 23;
@@ -268,7 +268,7 @@
         EVENT_NAMES.put(DAEMON_STARTUP, "DAEMON_STARTUP");
         EVENT_NAMES.put(ENABLE, "ENABLE");
         EVENT_NAMES.put(DISABLE, "DISABLE");
-        EVENT_NAMES.put(NATIVE_DAEMON_EVENT, "NATIVE_DAEMON_EVENT");
+        EVENT_NAMES.put(MDNS_SERVICE_EVENT, "MDNS_SERVICE_EVENT");
     }
 
     /** @hide */
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
index 8506db1..2621594 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -24,7 +24,6 @@
 import android.os.Parcelable;
 import android.text.TextUtils;
 import android.util.ArrayMap;
-import android.util.Base64;
 import android.util.Log;
 
 import java.io.UnsupportedEncodingException;
@@ -106,13 +105,11 @@
     /**
      * Unpack txt information from a base-64 encoded byte array.
      *
-     * @param rawRecords The raw base64 encoded records string read from netd.
+     * @param txtRecordsRawBytes The raw base64 encoded byte array.
      *
      * @hide
      */
-    public void setTxtRecords(@NonNull String rawRecords) {
-        byte[] txtRecordsRawBytes = Base64.decode(rawRecords, Base64.DEFAULT);
-
+    public void setTxtRecords(@NonNull byte[] txtRecordsRawBytes) {
         // There can be multiple TXT records after each other. Each record has to following format:
         //
         // byte                  type                  required   meaning
diff --git a/framework/Android.bp b/framework/Android.bp
index f31a7d5..d7de439 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -86,11 +86,13 @@
         "net-utils-device-common",
     ],
     static_libs: [
+        "mdns_aidl_interface-lateststable-java",
         "modules-utils-backgroundthread",
         "modules-utils-build",
         "modules-utils-preconditions",
     ],
     libs: [
+        "app-compat-annotations",
         "framework-connectivity-t.stubs.module_lib",
         "unsupportedappusage",
     ],
@@ -151,6 +153,11 @@
     ],
 }
 
+platform_compat_config {
+    name: "connectivity-platform-compat-config",
+    src: ":framework-connectivity",
+}
+
 cc_library_shared {
     name: "libframework-connectivity-jni",
     min_sdk_version: "30",
diff --git a/framework/src/android/net/LinkProperties.java b/framework/src/android/net/LinkProperties.java
index 99f48b4..8782b33 100644
--- a/framework/src/android/net/LinkProperties.java
+++ b/framework/src/android/net/LinkProperties.java
@@ -19,12 +19,16 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
+import android.app.compat.CompatChanges;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.os.Build;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.text.TextUtils;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.net.module.util.LinkPropertiesUtils;
 
 import java.net.Inet4Address;
@@ -38,6 +42,7 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.StringJoiner;
+import java.util.stream.Collectors;
 
 /**
  * Describes the properties of a network link.
@@ -52,6 +57,17 @@
  *
  */
 public final class LinkProperties implements Parcelable {
+    /**
+     * The {@link #getRoutes()} now can contain excluded as well as included routes. Use
+     * {@link RouteInfo#getType()} to determine route type.
+     *
+     * @hide
+     */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.S) // Switch to S_V2 when it is available.
+    @VisibleForTesting
+    public static final long EXCLUDED_ROUTES = 186082280;
+
     // The interface described by the network link.
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
     private String mIfaceName;
@@ -738,10 +754,25 @@
     /**
      * Returns all the {@link RouteInfo} set on this link.
      *
+     * Only unicast routes are returned for apps targeting Android S or below.
+     *
      * @return An unmodifiable {@link List} of {@link RouteInfo} for this link.
      */
     public @NonNull List<RouteInfo> getRoutes() {
-        return Collections.unmodifiableList(mRoutes);
+        if (CompatChanges.isChangeEnabled(EXCLUDED_ROUTES)) {
+            return Collections.unmodifiableList(mRoutes);
+        } else {
+            return Collections.unmodifiableList(getUnicastRoutes());
+        }
+    }
+
+    /**
+     * 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());
     }
 
     /**
@@ -757,11 +788,14 @@
 
     /**
      * Returns all the routes on this link and all the links stacked above it.
+     *
+     * Only unicast routes are returned for apps targeting Android S or below.
+     *
      * @hide
      */
     @SystemApi
     public @NonNull List<RouteInfo> getAllRoutes() {
-        List<RouteInfo> routes = new ArrayList<>(mRoutes);
+        final List<RouteInfo> routes = new ArrayList<>(getRoutes());
         for (LinkProperties stacked: mStackedLinks.values()) {
             routes.addAll(stacked.getAllRoutes());
         }
diff --git a/framework/src/android/net/NetworkAgentConfig.java b/framework/src/android/net/NetworkAgentConfig.java
index b28c006..0d2b620 100644
--- a/framework/src/android/net/NetworkAgentConfig.java
+++ b/framework/src/android/net/NetworkAgentConfig.java
@@ -24,6 +24,8 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import com.android.modules.utils.build.SdkLevel;
+
 import java.util.Objects;
 
 /**
@@ -473,6 +475,9 @@
         @NonNull
         @SystemApi(client = MODULE_LIBRARIES)
         public Builder setLocalRoutesExcludedForVpn(boolean excludeLocalRoutes) {
+            if (!SdkLevel.isAtLeastT()) {
+                throw new UnsupportedOperationException("Method is not supported");
+            }
             mConfig.excludeLocalRouteVpn = excludeLocalRoutes;
             return this;
         }
diff --git a/service-t/Android.bp b/service-t/Android.bp
index 0beb4c8..1b9f2ec 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -72,18 +72,3 @@
         "//packages/modules/IPsec/tests/iketests",
     ],
 }
-
-// Ethernet related libraries.
-// TODO: remove when ethernet tests are merged into connectivity tests
-filegroup {
-    name: "ethernet-service-test-sources",
-    srcs: [
-        "src/com/android/server/ethernet/**/*.java",
-        "src/com/android/server/net/DelayedDiskWrite.java",
-        "src/com/android/server/net/IpConfigStore.java",
-    ],
-    path: "src",
-    visibility: [
-        "//packages/modules/Connectivity/tests:__subpackages__",
-    ],
-}
diff --git a/service-t/Sources.bp b/service-t/Sources.bp
index 4e669b6..187eadf 100644
--- a/service-t/Sources.bp
+++ b/service-t/Sources.bp
@@ -14,27 +14,6 @@
 // limitations under the License.
 //
 
-// NetworkStats related libraries.
-
-filegroup {
-    name: "services.connectivity-netstats-sources",
-    srcs: [
-        "src/com/android/server/net/NetworkIdentity*.java",
-        "src/com/android/server/net/NetworkStats*.java",
-        "src/com/android/server/net/BpfInterfaceMapUpdater.java",
-        "src/com/android/server/net/InterfaceMapValue.java",
-        "src/com/android/server/net/CookieTagMapKey.java",
-        "src/com/android/server/net/CookieTagMapValue.java",
-        "src/com/android/server/net/StatsMapKey.java",
-        "src/com/android/server/net/StatsMapValue.java",
-        "src/com/android/server/net/UidStatsMapKey.java",
-    ],
-    path: "src",
-    visibility: [
-        "//visibility:private",
-    ],
-}
-
 // For test code only.
 filegroup {
     name: "lib_networkStatsFactory_native",
@@ -59,37 +38,3 @@
     ],
 }
 
-// Connectivity-T common libraries.
-
-// TODO: remove this empty filegroup.
-filegroup {
-    name: "services.connectivity-tiramisu-sources",
-    srcs: [],
-    path: "src",
-    visibility: ["//frameworks/base/services/core"],
-}
-
-cc_library_shared {
-    name: "libcom_android_net_module_util_jni",
-    min_sdk_version: "30",
-    cflags: [
-        "-Wall",
-        "-Werror",
-        "-Wno-unused-parameter",
-        "-Wthread-safety",
-    ],
-    srcs: [
-        "jni/onload.cpp",
-    ],
-    stl: "libc++_static",
-    static_libs: [
-        "libnet_utils_device_common_bpfjni",
-    ],
-    shared_libs: [
-        "liblog",
-        "libnativehelper",
-    ],
-    apex_available: [
-        "//apex_available:platform",
-    ],
-}
diff --git a/service-t/jni/onload.cpp b/service-t/jni/onload.cpp
deleted file mode 100644
index bca4697..0000000
--- a/service-t/jni/onload.cpp
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include <nativehelper/JNIHelp.h>
-#include <log/log.h>
-
-namespace android {
-
-int register_com_android_net_module_util_BpfMap(JNIEnv* env, char const* class_name);
-
-extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
-    JNIEnv *env;
-    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
-        ALOGE("GetEnv failed");
-        return JNI_ERR;
-    }
-
-    if (register_com_android_net_module_util_BpfMap(env,
-            "com/android/net/module/util/BpfMap") < 0) return JNI_ERR;
-
-    return JNI_VERSION_1_6;
-}
-
-};
-
diff --git a/service-t/src/com/android/server/ConnectivityServiceInitializer.java b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
index fa86f39..626c2eb 100644
--- a/service-t/src/com/android/server/ConnectivityServiceInitializer.java
+++ b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
@@ -21,6 +21,7 @@
 
 import com.android.modules.utils.build.SdkLevel;
 import com.android.networkstack.apishim.ConstantsShim;
+import com.android.server.connectivity.ConnectivityNativeService;
 import com.android.server.ethernet.EthernetService;
 import com.android.server.ethernet.EthernetServiceImpl;
 import com.android.server.nearby.NearbyService;
@@ -31,6 +32,7 @@
  */
 public final class ConnectivityServiceInitializer extends SystemService {
     private static final String TAG = ConnectivityServiceInitializer.class.getSimpleName();
+    private final ConnectivityNativeService mConnectivityNative;
     private final ConnectivityService mConnectivity;
     private final IpSecService mIpSecService;
     private final NsdService mNsdService;
@@ -44,6 +46,7 @@
         mEthernetServiceImpl = createEthernetService(context);
         mConnectivity = new ConnectivityService(context);
         mIpSecService = createIpSecService(context);
+        mConnectivityNative = createConnectivityNativeService(context);
         mNsdService = createNsdService(context);
         mNearbyService = createNearbyService(context);
     }
@@ -65,6 +68,12 @@
             publishBinderService(Context.IPSEC_SERVICE, mIpSecService, /* allowIsolated= */ false);
         }
 
+        if (mConnectivityNative != null) {
+            Log.i(TAG, "Registering " + ConnectivityNativeService.SERVICE_NAME);
+            publishBinderService(ConnectivityNativeService.SERVICE_NAME, mConnectivityNative,
+                    /* allowIsolated= */ false);
+        }
+
         if (mNsdService != null) {
             Log.i(TAG, "Registering " + Context.NSD_SERVICE);
             publishBinderService(Context.NSD_SERVICE, mNsdService, /* allowIsolated= */ false);
@@ -98,15 +107,24 @@
         return new IpSecService(context);
     }
 
+    /**
+     * Return ConnectivityNativeService instance, or null if current SDK is lower than T.
+     */
+    private ConnectivityNativeService createConnectivityNativeService(final Context context) {
+        if (!SdkLevel.isAtLeastT()) return null;
+        try {
+            return new ConnectivityNativeService(context);
+        } catch (UnsupportedOperationException e) {
+            Log.d(TAG, "Unable to get ConnectivityNative service", e);
+            return null;
+        }
+    }
+
     /** Return NsdService instance or null if current SDK is lower than T */
     private NsdService createNsdService(final Context context) {
         if (!SdkLevel.isAtLeastT()) return null;
-        try {
-            return NsdService.create(context);
-        } catch (InterruptedException e) {
-            Log.d(TAG, "Unable to get NSD service", e);
-            return null;
-        }
+
+        return NsdService.create(context);
     }
 
     /** Return Nearby service instance or null if current SDK is lower than T */
diff --git a/service-t/src/com/android/server/INativeDaemonConnectorCallbacks.java b/service-t/src/com/android/server/INativeDaemonConnectorCallbacks.java
deleted file mode 100644
index 0cf9dcd..0000000
--- a/service-t/src/com/android/server/INativeDaemonConnectorCallbacks.java
+++ /dev/null
@@ -1,25 +0,0 @@
-
-/*
- * Copyright (C) 2007 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;
-
-interface INativeDaemonConnectorCallbacks {
-
-    void onDaemonConnected();
-    boolean onCheckHoldWakeLock(int code);
-    boolean onEvent(int code, String raw, String[] cooked);
-}
diff --git a/service-t/src/com/android/server/NativeDaemonConnector.java b/service-t/src/com/android/server/NativeDaemonConnector.java
deleted file mode 100644
index ec8d779..0000000
--- a/service-t/src/com/android/server/NativeDaemonConnector.java
+++ /dev/null
@@ -1,704 +0,0 @@
-/*
- * Copyright (C) 2007 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server;
-
-import android.net.LocalSocket;
-import android.net.LocalSocketAddress;
-import android.os.Build;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.Message;
-import android.os.PowerManager;
-import android.os.SystemClock;
-import android.util.LocalLog;
-import android.util.Log;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.io.FileDescriptor;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.PrintWriter;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.LinkedList;
-import java.util.Objects;
-import java.util.concurrent.ArrayBlockingQueue;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * Generic connector class for interfacing with a native daemon which uses the
- * {@code libsysutils} FrameworkListener protocol.
- */
-final class NativeDaemonConnector implements Runnable, Handler.Callback {
-    private final static boolean VDBG = false;
-
-    private final String TAG;
-
-    private String mSocket;
-    private OutputStream mOutputStream;
-    private LocalLog mLocalLog;
-
-    private volatile boolean mDebug = false;
-    private volatile Object mWarnIfHeld;
-
-    private final ResponseQueue mResponseQueue;
-
-    private final PowerManager.WakeLock mWakeLock;
-
-    private final Looper mLooper;
-
-    private INativeDaemonConnectorCallbacks mCallbacks;
-    private Handler mCallbackHandler;
-
-    private AtomicInteger mSequenceNumber;
-
-    private static final long DEFAULT_TIMEOUT = 1 * 60 * 1000; /* 1 minute */
-    private static final long WARN_EXECUTE_DELAY_MS = 500; /* .5 sec */
-
-    /** Lock held whenever communicating with native daemon. */
-    private final Object mDaemonLock = new Object();
-
-    private final int BUFFER_SIZE = 4096;
-
-    NativeDaemonConnector(INativeDaemonConnectorCallbacks callbacks, String socket,
-            int responseQueueSize, String logTag, int maxLogSize, PowerManager.WakeLock wl) {
-        mCallbacks = callbacks;
-        mSocket = socket;
-        mResponseQueue = new ResponseQueue(responseQueueSize);
-        mWakeLock = wl;
-        if (mWakeLock != null) {
-            mWakeLock.setReferenceCounted(true);
-        }
-        mSequenceNumber = new AtomicInteger(0);
-        TAG = logTag != null ? logTag : "NativeDaemonConnector";
-        mLocalLog = new LocalLog(maxLogSize);
-        final HandlerThread thread = new HandlerThread(TAG);
-        thread.start();
-        mLooper = thread.getLooper();
-    }
-
-    /**
-     * Enable Set debugging mode, which causes messages to also be written to both
-     * {@link Log} in addition to internal log.
-     */
-    public void setDebug(boolean debug) {
-        mDebug = debug;
-    }
-
-    /**
-     * Like SystemClock.uptimeMillis, except truncated to an int so it will fit in a message arg.
-     * Inaccurate across 49.7 days of uptime, but only used for debugging.
-     */
-    private int uptimeMillisInt() {
-        return (int) SystemClock.uptimeMillis() & Integer.MAX_VALUE;
-    }
-
-    /**
-     * Yell loudly if someone tries making future {@link #execute(Command)}
-     * calls while holding a lock on the given object.
-     */
-    public void setWarnIfHeld(Object warnIfHeld) {
-        if (mWarnIfHeld != null) {
-            throw new IllegalStateException("warnIfHeld is already set.");
-        }
-        mWarnIfHeld = Objects.requireNonNull(warnIfHeld);
-    }
-
-    @Override
-    public void run() {
-        mCallbackHandler = new Handler(mLooper, this);
-
-        while (true) {
-            try {
-                listenToSocket();
-            } catch (Exception e) {
-                loge("Error in NativeDaemonConnector: " + e);
-                SystemClock.sleep(5000);
-            }
-        }
-    }
-
-    @Override
-    public boolean handleMessage(Message msg) {
-        final String event = (String) msg.obj;
-        final int start = uptimeMillisInt();
-        final int sent = msg.arg1;
-        try {
-            if (!mCallbacks.onEvent(msg.what, event, NativeDaemonEvent.unescapeArgs(event))) {
-                log(String.format("Unhandled event '%s'", event));
-            }
-        } catch (Exception e) {
-            loge("Error handling '" + event + "': " + e);
-        } finally {
-            if (mCallbacks.onCheckHoldWakeLock(msg.what) && mWakeLock != null) {
-                mWakeLock.release();
-            }
-            final int end = uptimeMillisInt();
-            if (start > sent && start - sent > WARN_EXECUTE_DELAY_MS) {
-                loge(String.format("NDC event {%s} processed too late: %dms", event, start - sent));
-            }
-            if (end > start && end - start > WARN_EXECUTE_DELAY_MS) {
-                loge(String.format("NDC event {%s} took too long: %dms", event, end - start));
-            }
-        }
-        return true;
-    }
-
-    private LocalSocketAddress determineSocketAddress() {
-        // If we're testing, set up a socket in a namespace that's accessible to test code.
-        // In order to ensure that unprivileged apps aren't able to impersonate native daemons on
-        // production devices, even if said native daemons ill-advisedly pick a socket name that
-        // starts with __test__, only allow this on debug builds.
-        if (mSocket.startsWith("__test__") && Build.isDebuggable()) {
-            return new LocalSocketAddress(mSocket);
-        } else {
-            return new LocalSocketAddress(mSocket, LocalSocketAddress.Namespace.RESERVED);
-        }
-    }
-
-    private void listenToSocket() throws IOException {
-        LocalSocket socket = null;
-
-        try {
-            socket = new LocalSocket();
-            LocalSocketAddress address = determineSocketAddress();
-
-            socket.connect(address);
-
-            InputStream inputStream = socket.getInputStream();
-            synchronized (mDaemonLock) {
-                mOutputStream = socket.getOutputStream();
-            }
-
-            mCallbacks.onDaemonConnected();
-
-            FileDescriptor[] fdList = null;
-            byte[] buffer = new byte[BUFFER_SIZE];
-            int start = 0;
-
-            while (true) {
-                int count = inputStream.read(buffer, start, BUFFER_SIZE - start);
-                if (count < 0) {
-                    loge("got " + count + " reading with start = " + start);
-                    break;
-                }
-                fdList = socket.getAncillaryFileDescriptors();
-
-                // Add our starting point to the count and reset the start.
-                count += start;
-                start = 0;
-
-                for (int i = 0; i < count; i++) {
-                    if (buffer[i] == 0) {
-                        // Note - do not log this raw message since it may contain
-                        // sensitive data
-                        final String rawEvent = new String(
-                                buffer, start, i - start, StandardCharsets.UTF_8);
-
-                        boolean releaseWl = false;
-                        try {
-                            final NativeDaemonEvent event =
-                                    NativeDaemonEvent.parseRawEvent(rawEvent, fdList);
-
-                            log("RCV <- {" + event + "}");
-
-                            if (event.isClassUnsolicited()) {
-                                // TODO: migrate to sending NativeDaemonEvent instances
-                                if (mCallbacks.onCheckHoldWakeLock(event.getCode())
-                                        && mWakeLock != null) {
-                                    mWakeLock.acquire();
-                                    releaseWl = true;
-                                }
-                                Message msg = mCallbackHandler.obtainMessage(
-                                        event.getCode(), uptimeMillisInt(), 0, event.getRawEvent());
-                                if (mCallbackHandler.sendMessage(msg)) {
-                                    releaseWl = false;
-                                }
-                            } else {
-                                mResponseQueue.add(event.getCmdNumber(), event);
-                            }
-                        } catch (IllegalArgumentException e) {
-                            log("Problem parsing message " + e);
-                        } finally {
-                            if (releaseWl) {
-                                mWakeLock.release();
-                            }
-                        }
-
-                        start = i + 1;
-                    }
-                }
-
-                if (start == 0) {
-                    log("RCV incomplete");
-                }
-
-                // We should end at the amount we read. If not, compact then
-                // buffer and read again.
-                if (start != count) {
-                    final int remaining = BUFFER_SIZE - start;
-                    System.arraycopy(buffer, start, buffer, 0, remaining);
-                    start = remaining;
-                } else {
-                    start = 0;
-                }
-            }
-        } catch (IOException ex) {
-            loge("Communications error: " + ex);
-            throw ex;
-        } finally {
-            synchronized (mDaemonLock) {
-                if (mOutputStream != null) {
-                    try {
-                        loge("closing stream for " + mSocket);
-                        mOutputStream.close();
-                    } catch (IOException e) {
-                        loge("Failed closing output stream: " + e);
-                    }
-                    mOutputStream = null;
-                }
-            }
-
-            try {
-                if (socket != null) {
-                    socket.close();
-                }
-            } catch (IOException ex) {
-                loge("Failed closing socket: " + ex);
-            }
-        }
-    }
-
-    /**
-     * Wrapper around argument that indicates it's sensitive and shouldn't be
-     * logged.
-     */
-    public static class SensitiveArg {
-        private final Object mArg;
-
-        public SensitiveArg(Object arg) {
-            mArg = arg;
-        }
-
-        @Override
-        public String toString() {
-            return String.valueOf(mArg);
-        }
-    }
-
-    /**
-     * Make command for daemon, escaping arguments as needed.
-     */
-    @VisibleForTesting
-    static void makeCommand(StringBuilder rawBuilder, StringBuilder logBuilder, int sequenceNumber,
-            String cmd, Object... args) {
-        if (cmd.indexOf('\0') >= 0) {
-            throw new IllegalArgumentException("Unexpected command: " + cmd);
-        }
-        if (cmd.indexOf(' ') >= 0) {
-            throw new IllegalArgumentException("Arguments must be separate from command");
-        }
-
-        rawBuilder.append(sequenceNumber).append(' ').append(cmd);
-        logBuilder.append(sequenceNumber).append(' ').append(cmd);
-        for (Object arg : args) {
-            final String argString = String.valueOf(arg);
-            if (argString.indexOf('\0') >= 0) {
-                throw new IllegalArgumentException("Unexpected argument: " + arg);
-            }
-
-            rawBuilder.append(' ');
-            logBuilder.append(' ');
-
-            appendEscaped(rawBuilder, argString);
-            if (arg instanceof SensitiveArg) {
-                logBuilder.append("[scrubbed]");
-            } else {
-                appendEscaped(logBuilder, argString);
-            }
-        }
-
-        rawBuilder.append('\0');
-    }
-
-    /**
-     * Method that waits until all asychronous notifications sent by the native daemon have
-     * been processed. This method must not be called on the notification thread or an
-     * exception will be thrown.
-     */
-    public void waitForCallbacks() {
-        if (Thread.currentThread() == mLooper.getThread()) {
-            throw new IllegalStateException("Must not call this method on callback thread");
-        }
-
-        final CountDownLatch latch = new CountDownLatch(1);
-        mCallbackHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                latch.countDown();
-            }
-        });
-        try {
-            latch.await();
-        } catch (InterruptedException e) {
-            Log.wtf(TAG, "Interrupted while waiting for unsolicited response handling", e);
-        }
-    }
-
-    /**
-     * Issue the given command to the native daemon and return a single expected
-     * response.
-     *
-     * @throws NativeDaemonConnectorException when problem communicating with
-     *             native daemon, or if the response matches
-     *             {@link NativeDaemonEvent#isClassClientError()} or
-     *             {@link NativeDaemonEvent#isClassServerError()}.
-     */
-    public NativeDaemonEvent execute(Command cmd) throws NativeDaemonConnectorException {
-        return execute(cmd.mCmd, cmd.mArguments.toArray());
-    }
-
-    /**
-     * Issue the given command to the native daemon and return a single expected
-     * response. Any arguments must be separated from base command so they can
-     * be properly escaped.
-     *
-     * @throws NativeDaemonConnectorException when problem communicating with
-     *             native daemon, or if the response matches
-     *             {@link NativeDaemonEvent#isClassClientError()} or
-     *             {@link NativeDaemonEvent#isClassServerError()}.
-     */
-    public NativeDaemonEvent execute(String cmd, Object... args)
-            throws NativeDaemonConnectorException {
-        return execute(DEFAULT_TIMEOUT, cmd, args);
-    }
-
-    public NativeDaemonEvent execute(long timeoutMs, String cmd, Object... args)
-            throws NativeDaemonConnectorException {
-        final NativeDaemonEvent[] events = executeForList(timeoutMs, cmd, args);
-        if (events.length != 1) {
-            throw new NativeDaemonConnectorException(
-                    "Expected exactly one response, but received " + events.length);
-        }
-        return events[0];
-    }
-
-    /**
-     * Issue the given command to the native daemon and return any
-     * {@link NativeDaemonEvent#isClassContinue()} responses, including the
-     * final terminal response.
-     *
-     * @throws NativeDaemonConnectorException when problem communicating with
-     *             native daemon, or if the response matches
-     *             {@link NativeDaemonEvent#isClassClientError()} or
-     *             {@link NativeDaemonEvent#isClassServerError()}.
-     */
-    public NativeDaemonEvent[] executeForList(Command cmd) throws NativeDaemonConnectorException {
-        return executeForList(cmd.mCmd, cmd.mArguments.toArray());
-    }
-
-    /**
-     * Issue the given command to the native daemon and return any
-     * {@link NativeDaemonEvent#isClassContinue()} responses, including the
-     * final terminal response. Any arguments must be separated from base
-     * command so they can be properly escaped.
-     *
-     * @throws NativeDaemonConnectorException when problem communicating with
-     *             native daemon, or if the response matches
-     *             {@link NativeDaemonEvent#isClassClientError()} or
-     *             {@link NativeDaemonEvent#isClassServerError()}.
-     */
-    public NativeDaemonEvent[] executeForList(String cmd, Object... args)
-            throws NativeDaemonConnectorException {
-        return executeForList(DEFAULT_TIMEOUT, cmd, args);
-    }
-
-    /**
-     * Issue the given command to the native daemon and return any {@linke
-     * NativeDaemonEvent@isClassContinue()} responses, including the final
-     * terminal response. Note that the timeout does not count time in deep
-     * sleep. Any arguments must be separated from base command so they can be
-     * properly escaped.
-     *
-     * @throws NativeDaemonConnectorException when problem communicating with
-     *             native daemon, or if the response matches
-     *             {@link NativeDaemonEvent#isClassClientError()} or
-     *             {@link NativeDaemonEvent#isClassServerError()}.
-     */
-    public NativeDaemonEvent[] executeForList(long timeoutMs, String cmd, Object... args)
-            throws NativeDaemonConnectorException {
-        if (mWarnIfHeld != null && Thread.holdsLock(mWarnIfHeld)) {
-            Log.wtf(TAG, "Calling thread " + Thread.currentThread().getName() + " is holding 0x"
-                    + Integer.toHexString(System.identityHashCode(mWarnIfHeld)), new Throwable());
-        }
-
-        final long startTime = SystemClock.elapsedRealtime();
-
-        final ArrayList<NativeDaemonEvent> events = new ArrayList<>();
-
-        final StringBuilder rawBuilder = new StringBuilder();
-        final StringBuilder logBuilder = new StringBuilder();
-        final int sequenceNumber = mSequenceNumber.incrementAndGet();
-
-        makeCommand(rawBuilder, logBuilder, sequenceNumber, cmd, args);
-
-        final String rawCmd = rawBuilder.toString();
-        final String logCmd = logBuilder.toString();
-
-        log("SND -> {" + logCmd + "}");
-
-        synchronized (mDaemonLock) {
-            if (mOutputStream == null) {
-                throw new NativeDaemonConnectorException("missing output stream");
-            } else {
-                try {
-                    mOutputStream.write(rawCmd.getBytes(StandardCharsets.UTF_8));
-                } catch (IOException e) {
-                    throw new NativeDaemonConnectorException("problem sending command", e);
-                }
-            }
-        }
-
-        NativeDaemonEvent event = null;
-        do {
-            event = mResponseQueue.remove(sequenceNumber, timeoutMs, logCmd);
-            if (event == null) {
-                loge("timed-out waiting for response to " + logCmd);
-                throw new NativeDaemonTimeoutException(logCmd, event);
-            }
-            if (VDBG) log("RMV <- {" + event + "}");
-            events.add(event);
-        } while (event.isClassContinue());
-
-        final long endTime = SystemClock.elapsedRealtime();
-        if (endTime - startTime > WARN_EXECUTE_DELAY_MS) {
-            loge("NDC Command {" + logCmd + "} took too long (" + (endTime - startTime) + "ms)");
-        }
-
-        if (event.isClassClientError()) {
-            throw new NativeDaemonArgumentException(logCmd, event);
-        }
-        if (event.isClassServerError()) {
-            throw new NativeDaemonFailureException(logCmd, event);
-        }
-
-        return events.toArray(new NativeDaemonEvent[events.size()]);
-    }
-
-    /**
-     * Append the given argument to {@link StringBuilder}, escaping as needed,
-     * and surrounding with quotes when it contains spaces.
-     */
-    @VisibleForTesting
-    static void appendEscaped(StringBuilder builder, String arg) {
-        final boolean hasSpaces = arg.indexOf(' ') >= 0;
-        if (hasSpaces) {
-            builder.append('"');
-        }
-
-        final int length = arg.length();
-        for (int i = 0; i < length; i++) {
-            final char c = arg.charAt(i);
-
-            if (c == '"') {
-                builder.append("\\\"");
-            } else if (c == '\\') {
-                builder.append("\\\\");
-            } else {
-                builder.append(c);
-            }
-        }
-
-        if (hasSpaces) {
-            builder.append('"');
-        }
-    }
-
-    private static class NativeDaemonArgumentException extends NativeDaemonConnectorException {
-        public NativeDaemonArgumentException(String command, NativeDaemonEvent event) {
-            super(command, event);
-        }
-
-        @Override
-        public IllegalArgumentException rethrowAsParcelableException() {
-            throw new IllegalArgumentException(getMessage(), this);
-        }
-    }
-
-    private static class NativeDaemonFailureException extends NativeDaemonConnectorException {
-        public NativeDaemonFailureException(String command, NativeDaemonEvent event) {
-            super(command, event);
-        }
-    }
-
-    /**
-     * Command builder that handles argument list building. Any arguments must
-     * be separated from base command so they can be properly escaped.
-     */
-    public static class Command {
-        private String mCmd;
-        private ArrayList<Object> mArguments = new ArrayList<>();
-
-        public Command(String cmd, Object... args) {
-            mCmd = cmd;
-            for (Object arg : args) {
-                appendArg(arg);
-            }
-        }
-
-        public Command appendArg(Object arg) {
-            mArguments.add(arg);
-            return this;
-        }
-    }
-
-    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
-        mLocalLog.dump(fd, pw, args);
-        pw.println();
-        mResponseQueue.dump(fd, pw, args);
-    }
-
-    private void log(String logstring) {
-        if (mDebug) Log.d(TAG, logstring);
-        mLocalLog.log(logstring);
-    }
-
-    private void loge(String logstring) {
-        Log.e(TAG, logstring);
-        mLocalLog.log(logstring);
-    }
-
-    private static class ResponseQueue {
-
-        private static class PendingCmd {
-            public final int cmdNum;
-            public final String logCmd;
-
-            public BlockingQueue<NativeDaemonEvent> responses =
-                    new ArrayBlockingQueue<NativeDaemonEvent>(10);
-
-            // The availableResponseCount member is used to track when we can remove this
-            // instance from the ResponseQueue.
-            // This is used under the protection of a sync of the mPendingCmds object.
-            // A positive value means we've had more writers retreive this object while
-            // a negative value means we've had more readers.  When we've had an equal number
-            // (it goes to zero) we can remove this object from the mPendingCmds list.
-            // Note that we may have more responses for this command (and more readers
-            // coming), but that would result in a new PendingCmd instance being created
-            // and added with the same cmdNum.
-            // Also note that when this goes to zero it just means a parity of readers and
-            // writers have retrieved this object - not that they are done using it.  The
-            // responses queue may well have more responses yet to be read or may get more
-            // responses added to it.  But all those readers/writers have retreived and
-            // hold references to this instance already so it can be removed from
-            // mPendingCmds queue.
-            public int availableResponseCount;
-
-            public PendingCmd(int cmdNum, String logCmd) {
-                this.cmdNum = cmdNum;
-                this.logCmd = logCmd;
-            }
-        }
-
-        private final LinkedList<PendingCmd> mPendingCmds;
-        private int mMaxCount;
-
-        ResponseQueue(int maxCount) {
-            mPendingCmds = new LinkedList<PendingCmd>();
-            mMaxCount = maxCount;
-        }
-
-        public void add(int cmdNum, NativeDaemonEvent response) {
-            PendingCmd found = null;
-            synchronized (mPendingCmds) {
-                for (PendingCmd pendingCmd : mPendingCmds) {
-                    if (pendingCmd.cmdNum == cmdNum) {
-                        found = pendingCmd;
-                        break;
-                    }
-                }
-                if (found == null) {
-                    // didn't find it - make sure our queue isn't too big before adding
-                    while (mPendingCmds.size() >= mMaxCount) {
-                        Log.e("NativeDaemonConnector.ResponseQueue",
-                                "more buffered than allowed: " + mPendingCmds.size() +
-                                " >= " + mMaxCount);
-                        // let any waiter timeout waiting for this
-                        PendingCmd pendingCmd = mPendingCmds.remove();
-                        Log.e("NativeDaemonConnector.ResponseQueue",
-                                "Removing request: " + pendingCmd.logCmd + " (" +
-                                pendingCmd.cmdNum + ")");
-                    }
-                    found = new PendingCmd(cmdNum, null);
-                    mPendingCmds.add(found);
-                }
-                found.availableResponseCount++;
-                // if a matching remove call has already retrieved this we can remove this
-                // instance from our list
-                if (found.availableResponseCount == 0) mPendingCmds.remove(found);
-            }
-            try {
-                found.responses.put(response);
-            } catch (InterruptedException e) { }
-        }
-
-        // note that the timeout does not count time in deep sleep.  If you don't want
-        // the device to sleep, hold a wakelock
-        public NativeDaemonEvent remove(int cmdNum, long timeoutMs, String logCmd) {
-            PendingCmd found = null;
-            synchronized (mPendingCmds) {
-                for (PendingCmd pendingCmd : mPendingCmds) {
-                    if (pendingCmd.cmdNum == cmdNum) {
-                        found = pendingCmd;
-                        break;
-                    }
-                }
-                if (found == null) {
-                    found = new PendingCmd(cmdNum, logCmd);
-                    mPendingCmds.add(found);
-                }
-                found.availableResponseCount--;
-                // if a matching add call has already retrieved this we can remove this
-                // instance from our list
-                if (found.availableResponseCount == 0) mPendingCmds.remove(found);
-            }
-            NativeDaemonEvent result = null;
-            try {
-                result = found.responses.poll(timeoutMs, TimeUnit.MILLISECONDS);
-            } catch (InterruptedException e) {}
-            if (result == null) {
-                Log.e("NativeDaemonConnector.ResponseQueue", "Timeout waiting for response");
-            }
-            return result;
-        }
-
-        public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
-            pw.println("Pending requests:");
-            synchronized (mPendingCmds) {
-                for (PendingCmd pendingCmd : mPendingCmds) {
-                    pw.println("  Cmd " + pendingCmd.cmdNum + " - " + pendingCmd.logCmd);
-                }
-            }
-        }
-    }
-}
diff --git a/service-t/src/com/android/server/NativeDaemonConnectorException.java b/service-t/src/com/android/server/NativeDaemonConnectorException.java
deleted file mode 100644
index 4d8881c..0000000
--- a/service-t/src/com/android/server/NativeDaemonConnectorException.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (C) 2006 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server;
-
-import android.os.Parcel;
-
-/**
- * An exception that indicates there was an error with a
- * {@link NativeDaemonConnector} operation.
- */
-public class NativeDaemonConnectorException extends Exception {
-    private String mCmd;
-    private NativeDaemonEvent mEvent;
-
-    public NativeDaemonConnectorException(String detailMessage) {
-        super(detailMessage);
-    }
-
-    public NativeDaemonConnectorException(String detailMessage, Throwable throwable) {
-        super(detailMessage, throwable);
-    }
-
-    public NativeDaemonConnectorException(String cmd, NativeDaemonEvent event) {
-        super("command '" + cmd + "' failed with '" + event + "'");
-        mCmd = cmd;
-        mEvent = event;
-    }
-
-    public int getCode() {
-        return mEvent != null ? mEvent.getCode() : -1;
-    }
-
-    public String getCmd() {
-        return mCmd;
-    }
-
-    /**
-     * Rethrow as a {@link RuntimeException} subclass that is handled by
-     * {@link Parcel#writeException(Exception)}.
-     */
-    public IllegalArgumentException rethrowAsParcelableException() {
-        throw new IllegalStateException(getMessage(), this);
-    }
-}
diff --git a/service-t/src/com/android/server/NativeDaemonEvent.java b/service-t/src/com/android/server/NativeDaemonEvent.java
deleted file mode 100644
index 5683694..0000000
--- a/service-t/src/com/android/server/NativeDaemonEvent.java
+++ /dev/null
@@ -1,267 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server;
-
-import android.util.Log;
-
-import java.io.FileDescriptor;
-import java.util.ArrayList;
-
-/**
- * Parsed event from native side of {@link NativeDaemonConnector}.
- */
-public class NativeDaemonEvent {
-
-    // TODO: keep class ranges in sync with ResponseCode.h
-    // TODO: swap client and server error ranges to roughly mirror HTTP spec
-
-    private final int mCmdNumber;
-    private final int mCode;
-    private final String mMessage;
-    private final String mRawEvent;
-    private final String mLogMessage;
-    private String[] mParsed;
-    private FileDescriptor[] mFdList;
-
-    private NativeDaemonEvent(int cmdNumber, int code, String message,
-                              String rawEvent, String logMessage, FileDescriptor[] fdList) {
-        mCmdNumber = cmdNumber;
-        mCode = code;
-        mMessage = message;
-        mRawEvent = rawEvent;
-        mLogMessage = logMessage;
-        mParsed = null;
-        mFdList = fdList;
-    }
-
-    static public final String SENSITIVE_MARKER = "{{sensitive}}";
-
-    public int getCmdNumber() {
-        return mCmdNumber;
-    }
-
-    public int getCode() {
-        return mCode;
-    }
-
-    public String getMessage() {
-        return mMessage;
-    }
-
-    public FileDescriptor[] getFileDescriptors() {
-        return mFdList;
-    }
-
-    @Deprecated
-    public String getRawEvent() {
-        return mRawEvent;
-    }
-
-    @Override
-    public String toString() {
-        return mLogMessage;
-    }
-
-    /**
-     * Test if event represents a partial response which is continued in
-     * additional subsequent events.
-     */
-    public boolean isClassContinue() {
-        return mCode >= 100 && mCode < 200;
-    }
-
-    /**
-     * Test if event represents a command success.
-     */
-    public boolean isClassOk() {
-        return mCode >= 200 && mCode < 300;
-    }
-
-    /**
-     * Test if event represents a remote native daemon error.
-     */
-    public boolean isClassServerError() {
-        return mCode >= 400 && mCode < 500;
-    }
-
-    /**
-     * Test if event represents a command syntax or argument error.
-     */
-    public boolean isClassClientError() {
-        return mCode >= 500 && mCode < 600;
-    }
-
-    /**
-     * Test if event represents an unsolicited event from native daemon.
-     */
-    public boolean isClassUnsolicited() {
-        return isClassUnsolicited(mCode);
-    }
-
-    private static boolean isClassUnsolicited(int code) {
-        return code >= 600 && code < 700;
-    }
-
-    /**
-     * Verify this event matches the given code.
-     *
-     * @throws IllegalStateException if {@link #getCode()} doesn't match.
-     */
-    public void checkCode(int code) {
-        if (mCode != code) {
-            throw new IllegalStateException("Expected " + code + " but was: " + this);
-        }
-    }
-
-    /**
-     * Parse the given raw event into {@link NativeDaemonEvent} instance.
-     *
-     * @throws IllegalArgumentException when line doesn't match format expected
-     *             from native side.
-     */
-    public static NativeDaemonEvent parseRawEvent(String rawEvent, FileDescriptor[] fdList) {
-        final String[] parsed = rawEvent.split(" ");
-        if (parsed.length < 2) {
-            throw new IllegalArgumentException("Insufficient arguments");
-        }
-
-        int skiplength = 0;
-
-        final int code;
-        try {
-            code = Integer.parseInt(parsed[0]);
-            skiplength = parsed[0].length() + 1;
-        } catch (NumberFormatException e) {
-            throw new IllegalArgumentException("problem parsing code", e);
-        }
-
-        int cmdNumber = -1;
-        if (isClassUnsolicited(code) == false) {
-            if (parsed.length < 3) {
-                throw new IllegalArgumentException("Insufficient arguemnts");
-            }
-            try {
-                cmdNumber = Integer.parseInt(parsed[1]);
-                skiplength += parsed[1].length() + 1;
-            } catch (NumberFormatException e) {
-                throw new IllegalArgumentException("problem parsing cmdNumber", e);
-            }
-        }
-
-        String logMessage = rawEvent;
-        if (parsed.length > 2 && parsed[2].equals(SENSITIVE_MARKER)) {
-            skiplength += parsed[2].length() + 1;
-            logMessage = parsed[0] + " " + parsed[1] + " {}";
-        }
-
-        final String message = rawEvent.substring(skiplength);
-
-        return new NativeDaemonEvent(cmdNumber, code, message, rawEvent, logMessage, fdList);
-    }
-
-    /**
-     * Filter the given {@link NativeDaemonEvent} list, returning
-     * {@link #getMessage()} for any events matching the requested code.
-     */
-    public static String[] filterMessageList(NativeDaemonEvent[] events, int matchCode) {
-        final ArrayList<String> result = new ArrayList<>();
-        for (NativeDaemonEvent event : events) {
-            if (event.getCode() == matchCode) {
-                result.add(event.getMessage());
-            }
-        }
-        return result.toArray(new String[result.size()]);
-    }
-
-    /**
-     * Find the Nth field of the event.
-     *
-     * This ignores and code or cmdNum, the first return value is given for N=0.
-     * Also understands "\"quoted\" multiword responses" and tries them as a single field
-     */
-    public String getField(int n) {
-        if (mParsed == null) {
-            mParsed = unescapeArgs(mRawEvent);
-        }
-        n += 2; // skip code and command#
-        if (n > mParsed.length) return null;
-            return mParsed[n];
-        }
-
-    public static String[] unescapeArgs(String rawEvent) {
-        final boolean DEBUG_ROUTINE = false;
-        final String LOGTAG = "unescapeArgs";
-        final ArrayList<String> parsed = new ArrayList<String>();
-        final int length = rawEvent.length();
-        int current = 0;
-        int wordEnd = -1;
-        boolean quoted = false;
-
-        if (DEBUG_ROUTINE) Log.e(LOGTAG, "parsing '" + rawEvent + "'");
-        if (rawEvent.charAt(current) == '\"') {
-            quoted = true;
-            current++;
-        }
-        while (current < length) {
-            // find the end of the word
-            char terminator = quoted ? '\"' : ' ';
-            wordEnd = current;
-            while (wordEnd < length && rawEvent.charAt(wordEnd) != terminator) {
-                if (rawEvent.charAt(wordEnd) == '\\') {
-                    // skip the escaped char
-                    ++wordEnd;
-                }
-                ++wordEnd;
-            }
-            if (wordEnd > length) wordEnd = length;
-            String word = rawEvent.substring(current, wordEnd);
-            current += word.length();
-            if (!quoted) {
-                word = word.trim();
-            } else {
-                current++;  // skip the trailing quote
-            }
-            // unescape stuff within the word
-            word = word.replace("\\\\", "\\");
-            word = word.replace("\\\"", "\"");
-
-            if (DEBUG_ROUTINE) Log.e(LOGTAG, "found '" + word + "'");
-            parsed.add(word);
-
-            // find the beginning of the next word - either of these options
-            int nextSpace = rawEvent.indexOf(' ', current);
-            int nextQuote = rawEvent.indexOf(" \"", current);
-            if (DEBUG_ROUTINE) {
-                Log.e(LOGTAG, "nextSpace=" + nextSpace + ", nextQuote=" + nextQuote);
-            }
-            if (nextQuote > -1 && nextQuote <= nextSpace) {
-                quoted = true;
-                current = nextQuote + 2;
-            } else {
-                quoted = false;
-                if (nextSpace > -1) {
-                    current = nextSpace + 1;
-                }
-            } // else we just start the next word after the current and read til the end
-            if (DEBUG_ROUTINE) {
-                Log.e(LOGTAG, "next loop - current=" + current
-                        + ", length=" + length + ", quoted=" + quoted);
-            }
-        }
-        return parsed.toArray(new String[parsed.size()]);
-    }
-}
diff --git a/service-t/src/com/android/server/NativeDaemonTimeoutException.java b/service-t/src/com/android/server/NativeDaemonTimeoutException.java
deleted file mode 100644
index 658f7d6..0000000
--- a/service-t/src/com/android/server/NativeDaemonTimeoutException.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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
- *
- *      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;
-
-/**
- * An exception that indicates there was a timeout with a
- * {@link NativeDaemonConnector} operation.
- */
-public class NativeDaemonTimeoutException extends NativeDaemonConnectorException {
-    public NativeDaemonTimeoutException(String command, NativeDaemonEvent event) {
-        super(command, event);
-    }
-}
-
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index ddf6d2c..4086e4e 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -16,15 +16,24 @@
 
 package com.android.server;
 
+import static android.net.ConnectivityManager.NETID_UNSET;
+import static android.net.nsd.NsdManager.MDNS_SERVICE_EVENT;
+
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.net.ConnectivityManager;
 import android.net.LinkProperties;
 import android.net.Network;
+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.INsdManager;
 import android.net.nsd.INsdManagerCallback;
 import android.net.nsd.INsdServiceConnector;
+import android.net.nsd.MDnsManager;
 import android.net.nsd.NsdManager;
 import android.net.nsd.NsdServiceInfo;
 import android.os.Handler;
@@ -33,7 +42,6 @@
 import android.os.Message;
 import android.os.RemoteException;
 import android.os.UserHandle;
-import android.util.Base64;
 import android.util.Log;
 import android.util.Pair;
 import android.util.SparseArray;
@@ -42,7 +50,6 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
-import com.android.net.module.util.DnsSdTxtRecord;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -50,9 +57,7 @@
 import java.net.NetworkInterface;
 import java.net.SocketException;
 import java.net.UnknownHostException;
-import java.util.Arrays;
 import java.util.HashMap;
-import java.util.concurrent.CountDownLatch;
 
 /**
  * Network Service Discovery Service handles remote service discovery operation requests by
@@ -64,14 +69,18 @@
     private static final String TAG = "NsdService";
     private static final String MDNS_TAG = "mDnsConnector";
 
-    private static final boolean DBG = true;
+    private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
     private static final long CLEANUP_DELAY_MS = 10000;
     private static final int IFACE_IDX_ANY = 0;
 
     private final Context mContext;
     private final NsdStateMachine mNsdStateMachine;
-    private final DaemonConnection mDaemon;
-    private final NativeCallbackReceiver mDaemonCallback;
+    private final MDnsManager mMDnsManager;
+    private final MDnsEventCallback mMDnsEventCallback;
+    // WARNING : Accessing this value in any thread is not safe, it must only be changed in the
+    // state machine thread. If change this outside state machine, it will need to introduce
+    // synchronization.
+    private boolean mIsDaemonStarted = false;
 
     /**
      * Clients receiving asynchronous messages
@@ -100,10 +109,26 @@
         }
 
         private void maybeStartDaemon() {
-            mDaemon.maybeStart();
+            if (mIsDaemonStarted) {
+                if (DBG) Log.d(TAG, "Daemon is already started.");
+                return;
+            }
+            mMDnsManager.registerEventListener(mMDnsEventCallback);
+            mMDnsManager.startDaemon();
+            mIsDaemonStarted = true;
             maybeScheduleStop();
         }
 
+        private void maybeStopDaemon() {
+            if (!mIsDaemonStarted) {
+                if (DBG) Log.d(TAG, "Daemon has not been started.");
+                return;
+            }
+            mMDnsManager.unregisterEventListener(mMDnsEventCallback);
+            mMDnsManager.stopDaemon();
+            mIsDaemonStarted = false;
+        }
+
         private boolean isAnyRequestActive() {
             return mIdToClientInfoMap.size() != 0;
         }
@@ -198,7 +223,7 @@
                         }
                         break;
                     case NsdManager.DAEMON_CLEANUP:
-                        mDaemon.maybeStop();
+                        maybeStopDaemon();
                         break;
                     // This event should be only sent by the legacy (target SDK < S) clients.
                     // Mark the sending client as legacy.
@@ -211,7 +236,6 @@
                             maybeStartDaemon();
                         }
                         break;
-                    case NsdManager.NATIVE_DAEMON_EVENT:
                     default:
                         Log.e(TAG, "Unhandled " + msg);
                         return NOT_HANDLED;
@@ -397,9 +421,8 @@
                                     clientId, NsdManager.FAILURE_INTERNAL_ERROR);
                         }
                         break;
-                    case NsdManager.NATIVE_DAEMON_EVENT:
-                        NativeEvent event = (NativeEvent) msg.obj;
-                        if (!handleNativeEvent(event.code, event.raw, event.cooked)) {
+                    case MDNS_SERVICE_EVENT:
+                        if (!handleMDnsServiceEvent(msg.arg1, msg.arg2, msg.obj)) {
                             return NOT_HANDLED;
                         }
                         break;
@@ -409,13 +432,11 @@
                 return HANDLED;
             }
 
-            private boolean handleNativeEvent(int code, String raw, String[] cooked) {
+            private boolean handleMDnsServiceEvent(int code, int id, Object obj) {
                 NsdServiceInfo servInfo;
-                int id = Integer.parseInt(cooked[1]);
                 ClientInfo clientInfo = mIdToClientInfoMap.get(id);
                 if (clientInfo == null) {
-                    String name = NativeResponseCode.nameOf(code);
-                    Log.e(TAG, String.format("id %d for %s has no client mapping", id, name));
+                    Log.e(TAG, String.format("id %d for %d has no client mapping", id, code));
                     return false;
                 }
 
@@ -425,27 +446,20 @@
                     // This can happen because of race conditions. For example,
                     // SERVICE_FOUND may race with STOP_SERVICE_DISCOVERY,
                     // and we may get in this situation.
-                    String name = NativeResponseCode.nameOf(code);
-                    Log.d(TAG, String.format(
-                            "Notification %s for listener id %d that is no longer active",
-                            name, id));
+                    Log.d(TAG, String.format("%d for listener id %d that is no longer active",
+                            code, id));
                     return false;
                 }
                 if (DBG) {
-                    String name = NativeResponseCode.nameOf(code);
-                    Log.d(TAG, String.format("Native daemon message %s: %s", name, raw));
+                    Log.d(TAG, String.format("MDns service event code:%d id=%d", code, id));
                 }
                 switch (code) {
-                    case NativeResponseCode.SERVICE_FOUND:
-                        /* NNN uniqueId serviceName regType domain interfaceIdx netId */
-                        servInfo = new NsdServiceInfo(cooked[2], cooked[3]);
-                        final int foundNetId;
-                        try {
-                            foundNetId = Integer.parseInt(cooked[6]);
-                        } catch (NumberFormatException e) {
-                            Log.wtf(TAG, "Invalid network received from mdnsd: " + cooked[6]);
-                            break;
-                        }
+                    case IMDnsEventListener.SERVICE_FOUND: {
+                        final DiscoveryInfo info = (DiscoveryInfo) obj;
+                        final String name = info.serviceName;
+                        final String type = info.registrationType;
+                        servInfo = new NsdServiceInfo(name, type);
+                        final int foundNetId = info.netId;
                         if (foundNetId == 0L) {
                             // Ignore services that do not have a Network: they are not usable
                             // by apps, as they would need privileged permissions to use
@@ -455,74 +469,65 @@
                         servInfo.setNetwork(new Network(foundNetId));
                         clientInfo.onServiceFound(clientId, servInfo);
                         break;
-                    case NativeResponseCode.SERVICE_LOST:
-                        /* NNN uniqueId serviceName regType domain interfaceIdx netId */
-                        final int lostNetId;
-                        try {
-                            lostNetId = Integer.parseInt(cooked[6]);
-                        } catch (NumberFormatException e) {
-                            Log.wtf(TAG, "Invalid network received from mdnsd: " + cooked[6]);
-                            break;
-                        }
-                        servInfo = new NsdServiceInfo(cooked[2], cooked[3]);
+                    }
+                    case IMDnsEventListener.SERVICE_LOST: {
+                        final DiscoveryInfo info = (DiscoveryInfo) obj;
+                        final String name = info.serviceName;
+                        final String type = info.registrationType;
+                        final int lostNetId = info.netId;
+                        servInfo = new NsdServiceInfo(name, type);
                         // The network could be null if it was torn down when the service is lost
                         // TODO: avoid returning null in that case, possibly by remembering found
                         // services on the same interface index and their network at the time
                         servInfo.setNetwork(lostNetId == 0 ? null : new Network(lostNetId));
                         clientInfo.onServiceLost(clientId, servInfo);
                         break;
-                    case NativeResponseCode.SERVICE_DISCOVERY_FAILED:
-                        /* NNN uniqueId errorCode */
+                    }
+                    case IMDnsEventListener.SERVICE_DISCOVERY_FAILED:
                         clientInfo.onDiscoverServicesFailed(
                                 clientId, NsdManager.FAILURE_INTERNAL_ERROR);
                         break;
-                    case NativeResponseCode.SERVICE_REGISTERED:
-                        /* NNN regId serviceName regType */
-                        servInfo = new NsdServiceInfo(cooked[2], null);
+                    case IMDnsEventListener.SERVICE_REGISTERED: {
+                        final RegistrationInfo info = (RegistrationInfo) obj;
+                        final String name = info.serviceName;
+                        servInfo = new NsdServiceInfo(name, null /* serviceType */);
                         clientInfo.onRegisterServiceSucceeded(clientId, servInfo);
                         break;
-                    case NativeResponseCode.SERVICE_REGISTRATION_FAILED:
-                        /* NNN regId errorCode */
+                    }
+                    case IMDnsEventListener.SERVICE_REGISTRATION_FAILED:
                         clientInfo.onRegisterServiceFailed(
                                 clientId, NsdManager.FAILURE_INTERNAL_ERROR);
                         break;
-                    case NativeResponseCode.SERVICE_UPDATED:
-                        /* NNN regId */
-                        break;
-                    case NativeResponseCode.SERVICE_UPDATE_FAILED:
-                        /* NNN regId errorCode */
-                        break;
-                    case NativeResponseCode.SERVICE_RESOLVED:
-                        /* NNN resolveId fullName hostName port txtlen txtdata interfaceIdx */
+                    case IMDnsEventListener.SERVICE_RESOLVED: {
+                        final ResolutionInfo info = (ResolutionInfo) obj;
                         int index = 0;
-                        while (index < cooked[2].length() && cooked[2].charAt(index) != '.') {
-                            if (cooked[2].charAt(index) == '\\') {
+                        final String fullName = info.serviceFullName;
+                        while (index < fullName.length() && fullName.charAt(index) != '.') {
+                            if (fullName.charAt(index) == '\\') {
                                 ++index;
                             }
                             ++index;
                         }
-                        if (index >= cooked[2].length()) {
-                            Log.e(TAG, "Invalid service found " + raw);
+                        if (index >= fullName.length()) {
+                            Log.e(TAG, "Invalid service found " + fullName);
                             break;
                         }
 
-                        String name = cooked[2].substring(0, index);
-                        String rest = cooked[2].substring(index);
+                        String name = fullName.substring(0, index);
+                        String rest = fullName.substring(index);
                         String type = rest.replace(".local.", "");
 
-                        name = unescape(name);
-
                         clientInfo.mResolvedService.setServiceName(name);
                         clientInfo.mResolvedService.setServiceType(type);
-                        clientInfo.mResolvedService.setPort(Integer.parseInt(cooked[4]));
-                        clientInfo.mResolvedService.setTxtRecords(cooked[6]);
+                        clientInfo.mResolvedService.setPort(info.port);
+                        clientInfo.mResolvedService.setTxtRecords(info.txtRecord);
                         // Network will be added after SERVICE_GET_ADDR_SUCCESS
 
                         stopResolveService(id);
                         removeRequestMap(clientId, id, clientInfo);
 
-                        int id2 = getUniqueId();
-                        if (getAddrInfo(id2, cooked[3], cooked[7] /* interfaceIdx */)) {
+                        final int id2 = getUniqueId();
+                        if (getAddrInfo(id2, info.hostname, info.interfaceIdx)) {
                             storeRequestMap(clientId, id2, clientInfo, NsdManager.RESOLVE_SERVICE);
                         } else {
                             clientInfo.onResolveServiceFailed(
@@ -530,7 +535,8 @@
                             clientInfo.mResolvedService = null;
                         }
                         break;
-                    case NativeResponseCode.SERVICE_RESOLUTION_FAILED:
+                    }
+                    case IMDnsEventListener.SERVICE_RESOLUTION_FAILED:
                         /* NNN resolveId errorCode */
                         stopResolveService(id);
                         removeRequestMap(clientId, id, clientInfo);
@@ -538,7 +544,7 @@
                         clientInfo.onResolveServiceFailed(
                                 clientId, NsdManager.FAILURE_INTERNAL_ERROR);
                         break;
-                    case NativeResponseCode.SERVICE_GET_ADDR_FAILED:
+                    case IMDnsEventListener.SERVICE_GET_ADDR_FAILED:
                         /* NNN resolveId errorCode */
                         stopGetAddrInfo(id);
                         removeRequestMap(clientId, id, clientInfo);
@@ -546,19 +552,15 @@
                         clientInfo.onResolveServiceFailed(
                                 clientId, NsdManager.FAILURE_INTERNAL_ERROR);
                         break;
-                    case NativeResponseCode.SERVICE_GET_ADDR_SUCCESS:
+                    case IMDnsEventListener.SERVICE_GET_ADDR_SUCCESS: {
                         /* NNN resolveId hostname ttl addr interfaceIdx netId */
-                        Network network = null;
-                        try {
-                            final int netId = Integer.parseInt(cooked[6]);
-                            network = netId == 0L ? null : new Network(netId);
-                        } catch (NumberFormatException e) {
-                            Log.wtf(TAG, "Invalid network in GET_ADDR_SUCCESS: " + cooked[6], e);
-                        }
-
+                        final GetAddressInfo info = (GetAddressInfo) obj;
+                        final String address = info.address;
+                        final int netId = info.netId;
+                        final Network network = netId == NETID_UNSET ? null : new Network(netId);
                         InetAddress serviceHost = null;
                         try {
-                            serviceHost = InetAddress.getByName(cooked[4]);
+                            serviceHost = InetAddress.getByName(address);
                         } catch (UnknownHostException e) {
                             Log.wtf(TAG, "Invalid host in GET_ADDR_SUCCESS", e);
                         }
@@ -579,6 +581,7 @@
                         removeRequestMap(clientId, id, clientInfo);
                         clientInfo.mResolvedService = null;
                         break;
+                    }
                     default:
                         return false;
                 }
@@ -587,50 +590,66 @@
        }
     }
 
-    private String unescape(String s) {
-        StringBuilder sb = new StringBuilder(s.length());
-        for (int i = 0; i < s.length(); ++i) {
-            char c = s.charAt(i);
-            if (c == '\\') {
-                if (++i >= s.length()) {
-                    Log.e(TAG, "Unexpected end of escape sequence in: " + s);
-                    break;
-                }
-                c = s.charAt(i);
-                if (c != '.' && c != '\\') {
-                    if (i + 2 >= s.length()) {
-                        Log.e(TAG, "Unexpected end of escape sequence in: " + s);
-                        break;
-                    }
-                    c = (char) ((c-'0') * 100 + (s.charAt(i+1)-'0') * 10 + (s.charAt(i+2)-'0'));
-                    i += 2;
-                }
-            }
-            sb.append(c);
-        }
-        return sb.toString();
-    }
-
     @VisibleForTesting
-    NsdService(Context ctx, Handler handler, DaemonConnectionSupplier fn, long cleanupDelayMs) {
+    NsdService(Context ctx, Handler handler, long cleanupDelayMs) {
         mCleanupDelayMs = cleanupDelayMs;
         mContext = ctx;
         mNsdStateMachine = new NsdStateMachine(TAG, handler);
         mNsdStateMachine.start();
-        mDaemonCallback = new NativeCallbackReceiver();
-        mDaemon = fn.get(mDaemonCallback);
+        mMDnsManager = ctx.getSystemService(MDnsManager.class);
+        mMDnsEventCallback = new MDnsEventCallback(mNsdStateMachine);
     }
 
-    public static NsdService create(Context context) throws InterruptedException {
+    public static NsdService create(Context context) {
         HandlerThread thread = new HandlerThread(TAG);
         thread.start();
         Handler handler = new Handler(thread.getLooper());
-        NsdService service =
-                new NsdService(context, handler, DaemonConnection::new, CLEANUP_DELAY_MS);
-        service.mDaemonCallback.awaitConnection();
+        NsdService service = new NsdService(context, handler, CLEANUP_DELAY_MS);
         return service;
     }
 
+    private static class MDnsEventCallback extends IMDnsEventListener.Stub {
+        private final StateMachine mStateMachine;
+
+        MDnsEventCallback(StateMachine sm) {
+            mStateMachine = sm;
+        }
+
+        @Override
+        public void onServiceRegistrationStatus(final RegistrationInfo status) {
+            mStateMachine.sendMessage(
+                    MDNS_SERVICE_EVENT, status.result, status.id, status);
+        }
+
+        @Override
+        public void onServiceDiscoveryStatus(final DiscoveryInfo status) {
+            mStateMachine.sendMessage(
+                    MDNS_SERVICE_EVENT, status.result, status.id, status);
+        }
+
+        @Override
+        public void onServiceResolutionStatus(final ResolutionInfo status) {
+            mStateMachine.sendMessage(
+                    MDNS_SERVICE_EVENT, status.result, status.id, status);
+        }
+
+        @Override
+        public void onGettingServiceAddressStatus(final GetAddressInfo status) {
+            mStateMachine.sendMessage(
+                    MDNS_SERVICE_EVENT, status.result, status.id, status);
+        }
+
+        @Override
+        public int getInterfaceVersion() throws RemoteException {
+            return this.VERSION;
+        }
+
+        @Override
+        public String getInterfaceHash() throws RemoteException {
+            return this.HASH;
+        }
+    }
+
     @Override
     public INsdServiceConnector connect(INsdManagerCallback cb) {
         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.INTERNET, "NsdService");
@@ -711,140 +730,6 @@
         return mUniqueId;
     }
 
-    /* These should be in sync with system/netd/server/ResponseCode.h */
-    static final class NativeResponseCode {
-        public static final int SERVICE_DISCOVERY_FAILED    =   602;
-        public static final int SERVICE_FOUND               =   603;
-        public static final int SERVICE_LOST                =   604;
-
-        public static final int SERVICE_REGISTRATION_FAILED =   605;
-        public static final int SERVICE_REGISTERED          =   606;
-
-        public static final int SERVICE_RESOLUTION_FAILED   =   607;
-        public static final int SERVICE_RESOLVED            =   608;
-
-        public static final int SERVICE_UPDATED             =   609;
-        public static final int SERVICE_UPDATE_FAILED       =   610;
-
-        public static final int SERVICE_GET_ADDR_FAILED     =   611;
-        public static final int SERVICE_GET_ADDR_SUCCESS    =   612;
-
-        private static final SparseArray<String> CODE_NAMES = new SparseArray<>();
-        static {
-            CODE_NAMES.put(SERVICE_DISCOVERY_FAILED, "SERVICE_DISCOVERY_FAILED");
-            CODE_NAMES.put(SERVICE_FOUND, "SERVICE_FOUND");
-            CODE_NAMES.put(SERVICE_LOST, "SERVICE_LOST");
-            CODE_NAMES.put(SERVICE_REGISTRATION_FAILED, "SERVICE_REGISTRATION_FAILED");
-            CODE_NAMES.put(SERVICE_REGISTERED, "SERVICE_REGISTERED");
-            CODE_NAMES.put(SERVICE_RESOLUTION_FAILED, "SERVICE_RESOLUTION_FAILED");
-            CODE_NAMES.put(SERVICE_RESOLVED, "SERVICE_RESOLVED");
-            CODE_NAMES.put(SERVICE_UPDATED, "SERVICE_UPDATED");
-            CODE_NAMES.put(SERVICE_UPDATE_FAILED, "SERVICE_UPDATE_FAILED");
-            CODE_NAMES.put(SERVICE_GET_ADDR_FAILED, "SERVICE_GET_ADDR_FAILED");
-            CODE_NAMES.put(SERVICE_GET_ADDR_SUCCESS, "SERVICE_GET_ADDR_SUCCESS");
-        }
-
-        static String nameOf(int code) {
-            String name = CODE_NAMES.get(code);
-            if (name == null) {
-                return Integer.toString(code);
-            }
-            return name;
-        }
-    }
-
-    private class NativeEvent {
-        final int code;
-        final String raw;
-        final String[] cooked;
-
-        NativeEvent(int code, String raw, String[] cooked) {
-            this.code = code;
-            this.raw = raw;
-            this.cooked = cooked;
-        }
-    }
-
-    class NativeCallbackReceiver implements INativeDaemonConnectorCallbacks {
-        private final CountDownLatch connected = new CountDownLatch(1);
-
-        public void awaitConnection() throws InterruptedException {
-            connected.await();
-        }
-
-        @Override
-        public void onDaemonConnected() {
-            connected.countDown();
-        }
-
-        @Override
-        public boolean onCheckHoldWakeLock(int code) {
-            return false;
-        }
-
-        @Override
-        public boolean onEvent(int code, String raw, String[] cooked) {
-            // TODO: NDC translates a message to a callback, we could enhance NDC to
-            // directly interact with a state machine through messages
-            NativeEvent event = new NativeEvent(code, raw, cooked);
-            mNsdStateMachine.sendMessage(NsdManager.NATIVE_DAEMON_EVENT, event);
-            return true;
-        }
-    }
-
-    interface DaemonConnectionSupplier {
-        DaemonConnection get(NativeCallbackReceiver callback);
-    }
-
-    @VisibleForTesting
-    public static class DaemonConnection {
-        final NativeDaemonConnector mNativeConnector;
-        boolean mIsStarted = false;
-
-        DaemonConnection(NativeCallbackReceiver callback) {
-            mNativeConnector = new NativeDaemonConnector(callback, "mdns", 10, MDNS_TAG, 25, null);
-            new Thread(mNativeConnector, MDNS_TAG).start();
-        }
-
-        /**
-         * Executes the specified cmd on the daemon.
-         */
-        public boolean execute(Object... args) {
-            if (DBG) {
-                Log.d(TAG, "mdnssd " + Arrays.toString(args));
-            }
-            try {
-                mNativeConnector.execute("mdnssd", args);
-            } catch (NativeDaemonConnectorException e) {
-                Log.e(TAG, "Failed to execute mdnssd " + Arrays.toString(args), e);
-                return false;
-            }
-            return true;
-        }
-
-        /**
-         * Starts the daemon if it is not already started.
-         */
-        public void maybeStart() {
-            if (mIsStarted) {
-                return;
-            }
-            execute("start-service");
-            mIsStarted = true;
-        }
-
-        /**
-         * Stops the daemon if it is started.
-         */
-        public void maybeStop() {
-            if (!mIsStarted) {
-                return;
-            }
-            execute("stop-service");
-            mIsStarted = false;
-        }
-    }
-
     private boolean registerService(int regId, NsdServiceInfo service) {
         if (DBG) {
             Log.d(TAG, "registerService: " + regId + " " + service);
@@ -853,34 +738,26 @@
         String type = service.getServiceType();
         int port = service.getPort();
         byte[] textRecord = service.getTxtRecord();
-        String record = Base64.encodeToString(textRecord, Base64.DEFAULT).replace("\n", "");
-        return mDaemon.execute("register", regId, name, type, port, record);
+        return mMDnsManager.registerService(regId, name, type, port, textRecord, IFACE_IDX_ANY);
     }
 
     private boolean unregisterService(int regId) {
-        return mDaemon.execute("stop-register", regId);
-    }
-
-    private boolean updateService(int regId, DnsSdTxtRecord t) {
-        if (t == null) {
-            return false;
-        }
-        return mDaemon.execute("update", regId, t.size(), t.getRawData());
+        return mMDnsManager.stopOperation(regId);
     }
 
     private boolean discoverServices(int discoveryId, NsdServiceInfo serviceInfo) {
         final Network network = serviceInfo.getNetwork();
+        final String type = serviceInfo.getServiceType();
         final int discoverInterface = getNetworkInterfaceIndex(network);
         if (network != null && discoverInterface == IFACE_IDX_ANY) {
             Log.e(TAG, "Interface to discover service on not found");
             return false;
         }
-        return mDaemon.execute("discover", discoveryId, serviceInfo.getServiceType(),
-                discoverInterface);
+        return mMDnsManager.discover(discoveryId, type, discoverInterface);
     }
 
     private boolean stopServiceDiscovery(int discoveryId) {
-        return mDaemon.execute("stop-discover", discoveryId);
+        return mMDnsManager.stopOperation(discoveryId);
     }
 
     private boolean resolveService(int resolveId, NsdServiceInfo service) {
@@ -892,7 +769,7 @@
             Log.e(TAG, "Interface to resolve service on not found");
             return false;
         }
-        return mDaemon.execute("resolve", resolveId, name, type, "local.", resolveInterface);
+        return mMDnsManager.resolve(resolveId, name, type, "local.", resolveInterface);
     }
 
     /**
@@ -933,16 +810,15 @@
     }
 
     private boolean stopResolveService(int resolveId) {
-        return mDaemon.execute("stop-resolve", resolveId);
+        return mMDnsManager.stopOperation(resolveId);
     }
 
-    private boolean getAddrInfo(int resolveId, String hostname, String interfaceIdx) {
-        // interfaceIdx is always obtained (as string) from the service resolved callback
-        return mDaemon.execute("getaddrinfo", resolveId, hostname, interfaceIdx);
+    private boolean getAddrInfo(int resolveId, String hostname, int interfaceIdx) {
+        return mMDnsManager.getServiceAddress(resolveId, hostname, interfaceIdx);
     }
 
     private boolean stopGetAddrInfo(int resolveId) {
-        return mDaemon.execute("stop-getaddrinfo", resolveId);
+        return mMDnsManager.stopOperation(resolveId);
     }
 
     @Override
diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
index d910629..fe27335 100644
--- a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
+++ b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
@@ -19,18 +19,16 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
-import android.content.res.Resources;
 import android.net.ConnectivityManager;
 import android.net.ConnectivityResources;
 import android.net.EthernetManager;
-import android.net.EthernetNetworkSpecifier;
 import android.net.EthernetNetworkManagementException;
+import android.net.EthernetNetworkSpecifier;
 import android.net.INetworkInterfaceOutcomeReceiver;
 import android.net.IpConfiguration;
 import android.net.IpConfiguration.IpAssignment;
 import android.net.IpConfiguration.ProxySettings;
 import android.net.LinkProperties;
-import android.net.Network;
 import android.net.NetworkAgentConfig;
 import android.net.NetworkCapabilities;
 import android.net.NetworkFactory;
@@ -72,8 +70,6 @@
 
     private final static int NETWORK_SCORE = 70;
     private static final String NETWORK_TYPE = "Ethernet";
-    private static final String LEGACY_TCP_BUFFER_SIZES =
-            "524288,1048576,3145728,524288,1048576,2097152";
 
     private final ConcurrentHashMap<String, NetworkInterfaceState> mTrackingInterfaces =
             new ConcurrentHashMap<>();
@@ -100,25 +96,9 @@
             return InterfaceParams.getByName(name);
         }
 
-        // TODO: remove legacy resource fallback after migrating its overlays.
-        private String getPlatformTcpBufferSizes(Context context) {
-            final Resources r = context.getResources();
-            final int resId = r.getIdentifier("config_ethernet_tcp_buffers", "string",
-                    context.getPackageName());
-            return r.getString(resId);
-        }
-
         public String getTcpBufferSizesFromResource(Context context) {
-            final String tcpBufferSizes;
-            final String platformTcpBufferSizes = getPlatformTcpBufferSizes(context);
-            if (!LEGACY_TCP_BUFFER_SIZES.equals(platformTcpBufferSizes)) {
-                // Platform resource is not the historical default: use the overlay.
-                tcpBufferSizes = platformTcpBufferSizes;
-            } else {
-                final ConnectivityResources resources = new ConnectivityResources(context);
-                tcpBufferSizes = resources.get().getString(R.string.config_ethernet_tcp_buffers);
-            }
-            return tcpBufferSizes;
+            final ConnectivityResources resources = new ConnectivityResources(context);
+            return resources.get().getString(R.string.config_ethernet_tcp_buffers);
         }
     }
 
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index c291b3f..e9053dd 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -25,12 +25,11 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
-import android.content.res.Resources;
 import android.net.ConnectivityResources;
 import android.net.EthernetManager;
 import android.net.IEthernetServiceListener;
-import android.net.INetworkInterfaceOutcomeReceiver;
 import android.net.INetd;
+import android.net.INetworkInterfaceOutcomeReceiver;
 import android.net.ITetheredInterfaceCallback;
 import android.net.InterfaceConfigurationParcel;
 import android.net.IpConfiguration;
@@ -57,6 +56,7 @@
 import java.io.FileDescriptor;
 import java.net.InetAddress;
 import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.ConcurrentHashMap;
@@ -85,7 +85,6 @@
     private static final boolean DBG = EthernetNetworkFactory.DBG;
 
     private static final String TEST_IFACE_REGEXP = TEST_TAP_PREFIX + "\\d+";
-    private static final String LEGACY_IFACE_REGEXP = "eth\\d";
 
     /**
      * Interface names we track. This is a product-dependent regular expression, plus,
@@ -133,48 +132,16 @@
     }
 
     public static class Dependencies {
-        // TODO: remove legacy resource fallback after migrating its overlays.
-        private String getPlatformRegexResource(Context context) {
-            final Resources r = context.getResources();
-            final int resId =
-                r.getIdentifier("config_ethernet_iface_regex", "string", context.getPackageName());
-            return r.getString(resId);
-        }
-
-        // TODO: remove legacy resource fallback after migrating its overlays.
-        private String[] getPlatformInterfaceConfigs(Context context) {
-            final Resources r = context.getResources();
-            final int resId = r.getIdentifier("config_ethernet_interfaces", "array",
-                    context.getPackageName());
-            return r.getStringArray(resId);
-        }
-
         public String getInterfaceRegexFromResource(Context context) {
-            final String platformRegex = getPlatformRegexResource(context);
-            final String match;
-            if (!LEGACY_IFACE_REGEXP.equals(platformRegex)) {
-                // Platform resource is not the historical default: use the overlay
-                match = platformRegex;
-            } else {
-                final ConnectivityResources resources = new ConnectivityResources(context);
-                match = resources.get().getString(
-                        com.android.connectivity.resources.R.string.config_ethernet_iface_regex);
-            }
-            return match;
+            final ConnectivityResources resources = new ConnectivityResources(context);
+            return resources.get().getString(
+                    com.android.connectivity.resources.R.string.config_ethernet_iface_regex);
         }
 
         public String[] getInterfaceConfigFromResource(Context context) {
-            final String[] platformInterfaceConfigs = getPlatformInterfaceConfigs(context);
-            final String[] interfaceConfigs;
-            if (platformInterfaceConfigs.length != 0) {
-                // Platform resource is not the historical default: use the overlay
-                interfaceConfigs = platformInterfaceConfigs;
-            } else {
-                final ConnectivityResources resources = new ConnectivityResources(context);
-                interfaceConfigs = resources.get().getStringArray(
-                        com.android.connectivity.resources.R.array.config_ethernet_interfaces);
-            }
-            return interfaceConfigs;
+            final ConnectivityResources resources = new ConnectivityResources(context);
+            return resources.get().getStringArray(
+                    com.android.connectivity.resources.R.array.config_ethernet_interfaces);
         }
     }
 
@@ -389,10 +356,33 @@
         mHandler.post(() -> {
             mIncludeTestInterfaces = include;
             updateIfaceMatchRegexp();
+            if (!include) {
+                removeTestData();
+            }
             mHandler.post(() -> trackAvailableInterfaces());
         });
     }
 
+    private void removeTestData() {
+        removeTestIpData();
+        removeTestCapabilityData();
+    }
+
+    private void removeTestIpData() {
+        final Iterator<String> iterator = mIpConfigurations.keySet().iterator();
+        while (iterator.hasNext()) {
+            final String iface = iterator.next();
+            if (iface.matches(TEST_IFACE_REGEXP)) {
+                mConfigStore.write(iface, null);
+                iterator.remove();
+            }
+        }
+    }
+
+    private void removeTestCapabilityData() {
+        mNetworkCapabilities.keySet().removeIf(iface -> iface.matches(TEST_IFACE_REGEXP));
+    }
+
     public void requestTetheredInterface(ITetheredInterfaceCallback callback) {
         mHandler.post(() -> {
             if (!mTetheredInterfaceRequests.register(callback)) {
diff --git a/service/Android.bp b/service/Android.bp
index 0e6fe92..25b970a 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -19,6 +19,54 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+aidl_interface {
+    name: "connectivity_native_aidl_interface",
+    local_include_dir: "binder",
+    vendor_available: true,
+    srcs: [
+        "binder/android/net/connectivity/aidl/*.aidl",
+    ],
+    backend: {
+        java: {
+            apex_available: [
+                "com.android.tethering",
+            ],
+            min_sdk_version: "30",
+        },
+        ndk: {
+            apex_available: [
+                "com.android.tethering",
+            ],
+            min_sdk_version: "30",
+        },
+    },
+    versions: ["1"],
+
+}
+
+cc_library_static {
+    name: "connectivity_native_aidl_interface-lateststable-ndk",
+    min_sdk_version: "30",
+    whole_static_libs: [
+        "connectivity_native_aidl_interface-V1-ndk",
+    ],
+    apex_available: [
+        "com.android.tethering",
+    ],
+}
+
+java_library {
+    name: "connectivity_native_aidl_interface-lateststable-java",
+    sdk_version: "system_current",
+    min_sdk_version: "30",
+    static_libs: [
+        "connectivity_native_aidl_interface-V1-java",
+    ],
+    apex_available: [
+        "com.android.tethering",
+    ],
+}
+
 // The library name match the service-connectivity jarjar rules that put the JNI utils in the
 // android.net.connectivity.com.android.net.module.util package.
 cc_library_shared {
@@ -35,6 +83,7 @@
     ],
     static_libs: [
         "libnet_utils_device_common_bpfjni",
+        "libnet_utils_device_common_bpfutils",
     ],
     shared_libs: [
         "liblog",
@@ -109,6 +158,7 @@
     static_libs: [
         // Do not add libs here if they are already included
         // in framework-connectivity
+        "connectivity_native_aidl_interface-lateststable-java",
         "dnsresolver_aidl_interface-V9-java",
         "modules-utils-shell-command-handler",
         "net-utils-device-common",
diff --git a/service/aidl_api/connectivity_native_aidl_interface/1/.hash b/service/aidl_api/connectivity_native_aidl_interface/1/.hash
new file mode 100644
index 0000000..4625b4b
--- /dev/null
+++ b/service/aidl_api/connectivity_native_aidl_interface/1/.hash
@@ -0,0 +1 @@
+037b467eb02b172a3161e11bbc3dd691aebb5fce
diff --git a/service/aidl_api/connectivity_native_aidl_interface/1/android/net/connectivity/aidl/ConnectivityNative.aidl b/service/aidl_api/connectivity_native_aidl_interface/1/android/net/connectivity/aidl/ConnectivityNative.aidl
new file mode 100644
index 0000000..b3985a4
--- /dev/null
+++ b/service/aidl_api/connectivity_native_aidl_interface/1/android/net/connectivity/aidl/ConnectivityNative.aidl
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.connectivity.aidl;
+interface ConnectivityNative {
+  void blockPortForBind(in int port);
+  void unblockPortForBind(in int port);
+  void unblockAllPortsForBind();
+  int[] getPortsBlockedForBind();
+}
diff --git a/service/aidl_api/connectivity_native_aidl_interface/current/android/net/connectivity/aidl/ConnectivityNative.aidl b/service/aidl_api/connectivity_native_aidl_interface/current/android/net/connectivity/aidl/ConnectivityNative.aidl
new file mode 100644
index 0000000..b3985a4
--- /dev/null
+++ b/service/aidl_api/connectivity_native_aidl_interface/current/android/net/connectivity/aidl/ConnectivityNative.aidl
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.connectivity.aidl;
+interface ConnectivityNative {
+  void blockPortForBind(in int port);
+  void unblockPortForBind(in int port);
+  void unblockAllPortsForBind();
+  int[] getPortsBlockedForBind();
+}
diff --git a/service/binder/android/net/connectivity/aidl/ConnectivityNative.aidl b/service/binder/android/net/connectivity/aidl/ConnectivityNative.aidl
new file mode 100644
index 0000000..31e24b4
--- /dev/null
+++ b/service/binder/android/net/connectivity/aidl/ConnectivityNative.aidl
@@ -0,0 +1,59 @@
+/**
+ * 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 android.net.connectivity.aidl;
+
+interface ConnectivityNative {
+    /**
+     * Blocks a port from being assigned during bind(). The caller is responsible for updating
+     * /proc/sys/net/ipv4/ip_local_port_range with the port being blocked so that calls to connect()
+     * will not automatically assign one of the blocked ports.
+     * Will return success even if port was already blocked.
+     *
+     * @param port Int corresponding to port number.
+     *
+     * @throws IllegalArgumentException if the port is invalid.
+     * @throws SecurityException if the UID of the client doesn't have network stack permission.
+     * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+     *         unix errno.
+     */
+    void blockPortForBind(in int port);
+
+    /**
+     * Unblocks a port that has previously been blocked.
+     * Will return success even if port was already unblocked.
+     *
+     * @param port Int corresponding to port number.
+     *
+     * @throws IllegalArgumentException if the port is invalid.
+     * @throws SecurityException if the UID of the client doesn't have network stack permission.
+     * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+     *         unix errno.
+     */
+    void unblockPortForBind(in int port);
+
+    /**
+     * Unblocks all ports that have previously been blocked.
+     */
+    void unblockAllPortsForBind();
+
+    /**
+     * Gets the list of ports that have been blocked.
+     *
+     * @return List of blocked ports.
+     */
+    int[] getPortsBlockedForBind();
+}
\ No newline at end of file
diff --git a/service/jarjar-rules.txt b/service/jarjar-rules.txt
index e90b29b..4b21569 100644
--- a/service/jarjar-rules.txt
+++ b/service/jarjar-rules.txt
@@ -108,5 +108,8 @@
 # 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
+
 # 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_net_module_util/onload.cpp b/service/jni/com_android_net_module_util/onload.cpp
index 2f09e55..d91eb03 100644
--- a/service/jni/com_android_net_module_util/onload.cpp
+++ b/service/jni/com_android_net_module_util/onload.cpp
@@ -21,6 +21,7 @@
 
 int register_com_android_net_module_util_BpfMap(JNIEnv* env, char const* class_name);
 int register_com_android_net_module_util_TcUtils(JNIEnv* env, char const* class_name);
+int register_com_android_net_module_util_BpfUtils(JNIEnv* env, char const* class_name);
 
 extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
     JNIEnv *env;
@@ -35,6 +36,9 @@
     if (register_com_android_net_module_util_TcUtils(env,
             "android/net/connectivity/com/android/net/module/util/TcUtils") < 0) return JNI_ERR;
 
+    if (register_com_android_net_module_util_BpfUtils(env,
+            "android/net/connectivity/com/android/net/module/util/BpfUtils") < 0) return JNI_ERR;
+
     return JNI_VERSION_1_6;
 }
 
diff --git a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
index 4517b5c..500c696 100644
--- a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
+++ b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
@@ -34,7 +34,6 @@
 #include <netjniutils/netjniutils.h>
 #include <private/android_filesystem_config.h>
 
-#include "libclat/bpfhelper.h"
 #include "libclat/clatutils.h"
 #include "nativehelper/scoped_utf_chars.h"
 
@@ -257,46 +256,6 @@
     }
 }
 
-int initTracker(const std::string& iface, const std::string& pfx96, const std::string& v4,
-        const std::string& v6, net::clat::ClatdTracker* output) {
-    strlcpy(output->iface, iface.c_str(), sizeof(output->iface));
-    output->ifIndex = if_nametoindex(iface.c_str());
-    if (output->ifIndex == 0) {
-        ALOGE("interface %s not found", output->iface);
-        return -1;
-    }
-
-    unsigned len = snprintf(output->v4iface, sizeof(output->v4iface),
-            "%s%s", DEVICEPREFIX, iface.c_str());
-    if (len >= sizeof(output->v4iface)) {
-        ALOGE("interface name too long '%s'", output->v4iface);
-        return -1;
-    }
-
-    output->v4ifIndex = if_nametoindex(output->v4iface);
-    if (output->v4ifIndex == 0) {
-        ALOGE("v4-interface %s not found", output->v4iface);
-        return -1;
-    }
-
-    if (!inet_pton(AF_INET6, pfx96.c_str(), &output->pfx96)) {
-        ALOGE("invalid IPv6 address specified for plat prefix: %s", pfx96.c_str());
-        return -1;
-    }
-
-    if (!inet_pton(AF_INET, v4.c_str(), &output->v4)) {
-        ALOGE("Invalid IPv4 address %s", v4.c_str());
-        return -1;
-    }
-
-    if (!inet_pton(AF_INET6, v6.c_str(), &output->v6)) {
-        ALOGE("Invalid source address %s", v6.c_str());
-        return -1;
-    }
-
-    return 0;
-}
-
 static jint com_android_server_connectivity_ClatCoordinator_startClatd(
         JNIEnv* env, jobject clazz, jobject tunJavaFd, jobject readSockJavaFd,
         jobject writeSockJavaFd, jstring iface, jstring pfx96, jstring v4, jstring v6) {
@@ -404,15 +363,6 @@
     posix_spawnattr_destroy(&attr);
     posix_spawn_file_actions_destroy(&fa);
 
-    // 6. Start BPF if any
-    if (!net::clat::initMaps()) {
-        net::clat::ClatdTracker tracker = {};
-        if (!initTracker(ifaceStr.c_str(), pfx96Str.c_str(), v4Str.c_str(), v6Str.c_str(),
-                &tracker)) {
-            net::clat::maybeStartBpf(tracker);
-        }
-    }
-
     return pid;
 }
 
@@ -467,14 +417,6 @@
         return;
     }
 
-    if (!net::clat::initMaps()) {
-        net::clat::ClatdTracker tracker = {};
-        if (!initTracker(ifaceStr.c_str(), pfx96Str.c_str(), v4Str.c_str(), v6Str.c_str(),
-                &tracker)) {
-            net::clat::maybeStopBpf(tracker);
-        }
-    }
-
     stopClatdProcess(pid);
 }
 
diff --git a/service/native/libs/libclat/Android.bp b/service/native/libs/libclat/Android.bp
index 17ee996..68e4dc4 100644
--- a/service/native/libs/libclat/Android.bp
+++ b/service/native/libs/libclat/Android.bp
@@ -19,19 +19,12 @@
 cc_library_static {
     name: "libclat",
     defaults: ["netd_defaults"],
-    header_libs: [
-        "bpf_connectivity_headers",
-        "libbase_headers",
-    ],
     srcs: [
-        "TcUtils.cpp",  // TODO: move to frameworks/libs/net
-        "bpfhelper.cpp",
         "clatutils.cpp",
     ],
     stl: "libc++_static",
     static_libs: [
         "libip_checksum",
-        "libnetdutils",  // for netdutils/UidConstants.h in bpf_shared.h
     ],
     shared_libs: ["liblog"],
     export_include_dirs: ["include"],
@@ -43,11 +36,7 @@
     name: "libclat_test",
     defaults: ["netd_defaults"],
     test_suites: ["device-tests"],
-    header_libs: [
-        "bpf_connectivity_headers",
-    ],
     srcs: [
-        "TcUtilsTest.cpp",
         "clatutils_test.cpp",
     ],
     static_libs: [
@@ -55,8 +44,6 @@
         "libclat",
         "libip_checksum",
         "libnetd_test_tun_interface",
-        "libnetdutils",  // for netdutils/UidConstants.h in bpf_shared.h
-        "libtcutils",
     ],
     shared_libs: [
         "liblog",
diff --git a/service/native/libs/libclat/TcUtils.cpp b/service/native/libs/libclat/TcUtils.cpp
deleted file mode 100644
index cdfb763..0000000
--- a/service/native/libs/libclat/TcUtils.cpp
+++ /dev/null
@@ -1,390 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#define LOG_TAG "TcUtils"
-
-#include "libclat/TcUtils.h"
-
-#include <arpa/inet.h>
-#include <linux/if.h>
-#include <linux/if_arp.h>
-#include <linux/netlink.h>
-#include <linux/pkt_cls.h>
-#include <linux/pkt_sched.h>
-#include <sys/ioctl.h>
-#include <sys/socket.h>
-#include <sys/types.h>
-#include <unistd.h>
-
-#include <log/log.h>
-
-#include "android-base/unique_fd.h"
-
-namespace android {
-namespace net {
-
-using std::max;
-
-// Sync from system/netd/server/NetlinkCommands.h
-const sockaddr_nl KERNEL_NLADDR = {AF_NETLINK, 0, 0, 0};
-const uint16_t NETLINK_REQUEST_FLAGS = NLM_F_REQUEST | NLM_F_ACK;
-
-static int doSIOCGIF(const std::string& interface, int opt) {
-    base::unique_fd ufd(socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0));
-
-    if (ufd < 0) {
-        const int err = errno;
-        ALOGE("socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0)");
-        return -err;
-    };
-
-    struct ifreq ifr = {};
-    // We use strncpy() instead of strlcpy() since kernel has to be able
-    // to handle non-zero terminated junk passed in by userspace anyway,
-    // and this way too long interface names (more than IFNAMSIZ-1 = 15
-    // characters plus terminating NULL) will not get truncated to 15
-    // characters and zero-terminated and thus potentially erroneously
-    // match a truncated interface if one were to exist.
-    strncpy(ifr.ifr_name, interface.c_str(), sizeof(ifr.ifr_name));
-
-    if (ioctl(ufd, opt, &ifr, sizeof(ifr))) return -errno;
-
-    if (opt == SIOCGIFHWADDR) return ifr.ifr_hwaddr.sa_family;
-    if (opt == SIOCGIFMTU) return ifr.ifr_mtu;
-    return -EINVAL;
-}
-
-int hardwareAddressType(const std::string& interface) {
-    return doSIOCGIF(interface, SIOCGIFHWADDR);
-}
-
-int deviceMTU(const std::string& interface) {
-    return doSIOCGIF(interface, SIOCGIFMTU);
-}
-
-base::Result<bool> isEthernet(const std::string& interface) {
-    int rv = hardwareAddressType(interface);
-    if (rv < 0) {
-        errno = -rv;
-        return ErrnoErrorf("Get hardware address type of interface {} failed", interface);
-    }
-
-    switch (rv) {
-        case ARPHRD_ETHER:
-            return true;
-        case ARPHRD_NONE:
-        case ARPHRD_RAWIP:  // in Linux 4.14+ rmnet support was upstreamed and this is 519
-        case 530:           // this is ARPHRD_RAWIP on some Android 4.9 kernels with rmnet
-            return false;
-        default:
-            errno = EAFNOSUPPORT;  // Address family not supported
-            return ErrnoErrorf("Unknown hardware address type {} on interface {}", rv, interface);
-    }
-}
-
-// TODO: use //system/netd/server/NetlinkCommands.cpp:openNetlinkSocket(protocol)
-// and //system/netd/server/SockDiag.cpp:checkError(fd)
-static int sendAndProcessNetlinkResponse(const void* req, int len) {
-    base::unique_fd fd(socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE));
-    if (fd == -1) {
-        const int err = errno;
-        ALOGE("socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE)");
-        return -err;
-    }
-
-    static constexpr int on = 1;
-    int rv = setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, &on, sizeof(on));
-    if (rv) ALOGE("setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, %d)", on);
-
-    // this is needed to get sane strace netlink parsing, it allocates the pid
-    rv = bind(fd, (const struct sockaddr*)&KERNEL_NLADDR, sizeof(KERNEL_NLADDR));
-    if (rv) {
-        const int err = errno;
-        ALOGE("bind(fd, {AF_NETLINK, 0, 0})");
-        return -err;
-    }
-
-    // we do not want to receive messages from anyone besides the kernel
-    rv = connect(fd, (const struct sockaddr*)&KERNEL_NLADDR, sizeof(KERNEL_NLADDR));
-    if (rv) {
-        const int err = errno;
-        ALOGE("connect(fd, {AF_NETLINK, 0, 0})");
-        return -err;
-    }
-
-    rv = send(fd, req, len, 0);
-    if (rv == -1) return -errno;
-    if (rv != len) return -EMSGSIZE;
-
-    struct {
-        nlmsghdr h;
-        nlmsgerr e;
-        char buf[256];
-    } resp = {};
-
-    rv = recv(fd, &resp, sizeof(resp), MSG_TRUNC);
-
-    if (rv == -1) {
-        const int err = errno;
-        ALOGE("recv() failed");
-        return -err;
-    }
-
-    if (rv < (int)NLMSG_SPACE(sizeof(struct nlmsgerr))) {
-        ALOGE("recv() returned short packet: %d", rv);
-        return -EMSGSIZE;
-    }
-
-    if (resp.h.nlmsg_len != (unsigned)rv) {
-        ALOGE("recv() returned invalid header length: %d != %d", resp.h.nlmsg_len, rv);
-        return -EBADMSG;
-    }
-
-    if (resp.h.nlmsg_type != NLMSG_ERROR) {
-        ALOGE("recv() did not return NLMSG_ERROR message: %d", resp.h.nlmsg_type);
-        return -EBADMSG;
-    }
-
-    return resp.e.error;  // returns 0 on success
-}
-
-// ADD:     nlMsgType=RTM_NEWQDISC nlMsgFlags=NLM_F_EXCL|NLM_F_CREATE
-// REPLACE: nlMsgType=RTM_NEWQDISC nlMsgFlags=NLM_F_CREATE|NLM_F_REPLACE
-// DEL:     nlMsgType=RTM_DELQDISC nlMsgFlags=0
-int doTcQdiscClsact(int ifIndex, uint16_t nlMsgType, uint16_t nlMsgFlags) {
-    // This is the name of the qdisc we are attaching.
-    // Some hoop jumping to make this compile time constant with known size,
-    // so that the structure declaration is well defined at compile time.
-#define CLSACT "clsact"
-    // sizeof() includes the terminating NULL
-    static constexpr size_t ASCIIZ_LEN_CLSACT = sizeof(CLSACT);
-
-    const struct {
-        nlmsghdr n;
-        tcmsg t;
-        struct {
-            nlattr attr;
-            char str[NLMSG_ALIGN(ASCIIZ_LEN_CLSACT)];
-        } kind;
-    } req = {
-            .n =
-                    {
-                            .nlmsg_len = sizeof(req),
-                            .nlmsg_type = nlMsgType,
-                            .nlmsg_flags = static_cast<__u16>(NETLINK_REQUEST_FLAGS | nlMsgFlags),
-                    },
-            .t =
-                    {
-                            .tcm_family = AF_UNSPEC,
-                            .tcm_ifindex = ifIndex,
-                            .tcm_handle = TC_H_MAKE(TC_H_CLSACT, 0),
-                            .tcm_parent = TC_H_CLSACT,
-                    },
-            .kind =
-                    {
-                            .attr =
-                                    {
-                                            .nla_len = NLA_HDRLEN + ASCIIZ_LEN_CLSACT,
-                                            .nla_type = TCA_KIND,
-                                    },
-                            .str = CLSACT,
-                    },
-    };
-#undef CLSACT
-
-    return sendAndProcessNetlinkResponse(&req, sizeof(req));
-}
-
-// tc filter add dev .. in/egress prio 4 protocol ipv6/ip bpf object-pinned /sys/fs/bpf/...
-// direct-action
-int tcFilterAddDevBpf(int ifIndex, bool ingress, uint16_t proto, int bpfFd, bool ethernet) {
-    // This is the name of the filter we're attaching (ie. this is the 'bpf'
-    // packet classifier enabled by kernel config option CONFIG_NET_CLS_BPF.
-    //
-    // We go through some hoops in order to make this compile time constants
-    // so that we can define the struct further down the function with the
-    // field for this sized correctly already during the build.
-#define BPF "bpf"
-    // sizeof() includes the terminating NULL
-    static constexpr size_t ASCIIZ_LEN_BPF = sizeof(BPF);
-
-    // This is to replicate program name suffix used by 'tc' Linux cli
-    // when it attaches programs.
-#define FSOBJ_SUFFIX ":[*fsobj]"
-
-    // This macro expands (from header files) to:
-    //   prog_clatd_schedcls_ingress6_clat_rawip:[*fsobj]
-    // and is the name of the pinned ingress ebpf program for ARPHRD_RAWIP interfaces.
-    // (also compatible with anything that has 0 size L2 header)
-    static constexpr char name_clat_rx_rawip[] = CLAT_INGRESS6_PROG_RAWIP_NAME FSOBJ_SUFFIX;
-
-    // This macro expands (from header files) to:
-    //   prog_clatd_schedcls_ingress6_clat_ether:[*fsobj]
-    // and is the name of the pinned ingress ebpf program for ARPHRD_ETHER interfaces.
-    // (also compatible with anything that has standard ethernet header)
-    static constexpr char name_clat_rx_ether[] = CLAT_INGRESS6_PROG_ETHER_NAME FSOBJ_SUFFIX;
-
-    // This macro expands (from header files) to:
-    //   prog_clatd_schedcls_egress4_clat_rawip:[*fsobj]
-    // and is the name of the pinned egress ebpf program for ARPHRD_RAWIP interfaces.
-    // (also compatible with anything that has 0 size L2 header)
-    static constexpr char name_clat_tx_rawip[] = CLAT_EGRESS4_PROG_RAWIP_NAME FSOBJ_SUFFIX;
-
-    // This macro expands (from header files) to:
-    //   prog_clatd_schedcls_egress4_clat_ether:[*fsobj]
-    // and is the name of the pinned egress ebpf program for ARPHRD_ETHER interfaces.
-    // (also compatible with anything that has standard ethernet header)
-    static constexpr char name_clat_tx_ether[] = CLAT_EGRESS4_PROG_ETHER_NAME FSOBJ_SUFFIX;
-
-#undef FSOBJ_SUFFIX
-
-    // The actual name we'll use is determined at run time via 'ethernet' and 'ingress'
-    // booleans.  We need to compile time allocate enough space in the struct
-    // hence this macro magic to make sure we have enough space for either
-    // possibility.  In practice some of these are actually the same size.
-    static constexpr size_t ASCIIZ_MAXLEN_NAME = max({
-            sizeof(name_clat_rx_rawip),
-            sizeof(name_clat_rx_ether),
-            sizeof(name_clat_tx_rawip),
-            sizeof(name_clat_tx_ether),
-    });
-
-    // These are not compile time constants: 'name' is used in strncpy below
-    const char* const name_clat_rx = ethernet ? name_clat_rx_ether : name_clat_rx_rawip;
-    const char* const name_clat_tx = ethernet ? name_clat_tx_ether : name_clat_tx_rawip;
-    const char* const name = ingress ? name_clat_rx : name_clat_tx;
-
-    struct {
-        nlmsghdr n;
-        tcmsg t;
-        struct {
-            nlattr attr;
-            char str[NLMSG_ALIGN(ASCIIZ_LEN_BPF)];
-        } kind;
-        struct {
-            nlattr attr;
-            struct {
-                nlattr attr;
-                __u32 u32;
-            } fd;
-            struct {
-                nlattr attr;
-                char str[NLMSG_ALIGN(ASCIIZ_MAXLEN_NAME)];
-            } name;
-            struct {
-                nlattr attr;
-                __u32 u32;
-            } flags;
-        } options;
-    } req = {
-            .n =
-                    {
-                            .nlmsg_len = sizeof(req),
-                            .nlmsg_type = RTM_NEWTFILTER,
-                            .nlmsg_flags = NETLINK_REQUEST_FLAGS | NLM_F_EXCL | NLM_F_CREATE,
-                    },
-            .t =
-                    {
-                            .tcm_family = AF_UNSPEC,
-                            .tcm_ifindex = ifIndex,
-                            .tcm_handle = TC_H_UNSPEC,
-                            .tcm_parent = TC_H_MAKE(TC_H_CLSACT,
-                                                    ingress ? TC_H_MIN_INGRESS : TC_H_MIN_EGRESS),
-                            .tcm_info = static_cast<__u32>((PRIO_CLAT << 16) | htons(proto)),
-                    },
-            .kind =
-                    {
-                            .attr =
-                                    {
-                                            .nla_len = sizeof(req.kind),
-                                            .nla_type = TCA_KIND,
-                                    },
-                            .str = BPF,
-                    },
-            .options =
-                    {
-                            .attr =
-                                    {
-                                            .nla_len = sizeof(req.options),
-                                            .nla_type = NLA_F_NESTED | TCA_OPTIONS,
-                                    },
-                            .fd =
-                                    {
-                                            .attr =
-                                                    {
-                                                            .nla_len = sizeof(req.options.fd),
-                                                            .nla_type = TCA_BPF_FD,
-                                                    },
-                                            .u32 = static_cast<__u32>(bpfFd),
-                                    },
-                            .name =
-                                    {
-                                            .attr =
-                                                    {
-                                                            .nla_len = sizeof(req.options.name),
-                                                            .nla_type = TCA_BPF_NAME,
-                                                    },
-                                            // Visible via 'tc filter show', but
-                                            // is overwritten by strncpy below
-                                            .str = "placeholder",
-                                    },
-                            .flags =
-                                    {
-                                            .attr =
-                                                    {
-                                                            .nla_len = sizeof(req.options.flags),
-                                                            .nla_type = TCA_BPF_FLAGS,
-                                                    },
-                                            .u32 = TCA_BPF_FLAG_ACT_DIRECT,
-                                    },
-                    },
-    };
-#undef BPF
-
-    strncpy(req.options.name.str, name, sizeof(req.options.name.str));
-
-    return sendAndProcessNetlinkResponse(&req, sizeof(req));
-}
-
-// tc filter del dev .. in/egress prio 4 protocol ..
-int tcFilterDelDev(int ifIndex, bool ingress, uint16_t prio, uint16_t proto) {
-    const struct {
-        nlmsghdr n;
-        tcmsg t;
-    } req = {
-            .n =
-                    {
-                            .nlmsg_len = sizeof(req),
-                            .nlmsg_type = RTM_DELTFILTER,
-                            .nlmsg_flags = NETLINK_REQUEST_FLAGS,
-                    },
-            .t =
-                    {
-                            .tcm_family = AF_UNSPEC,
-                            .tcm_ifindex = ifIndex,
-                            .tcm_handle = TC_H_UNSPEC,
-                            .tcm_parent = TC_H_MAKE(TC_H_CLSACT,
-                                                    ingress ? TC_H_MIN_INGRESS : TC_H_MIN_EGRESS),
-                            .tcm_info = (static_cast<uint32_t>(prio) << 16) |
-                                        static_cast<uint32_t>(htons(proto)),
-                    },
-    };
-
-    return sendAndProcessNetlinkResponse(&req, sizeof(req));
-}
-
-}  // namespace net
-}  // namespace android
diff --git a/service/native/libs/libclat/TcUtilsTest.cpp b/service/native/libs/libclat/TcUtilsTest.cpp
deleted file mode 100644
index 08f3042..0000000
--- a/service/native/libs/libclat/TcUtilsTest.cpp
+++ /dev/null
@@ -1,212 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- * TcUtilsTest.cpp - unit tests for TcUtils.cpp
- */
-
-#include <gtest/gtest.h>
-
-#include "libclat/TcUtils.h"
-
-#include <linux/if_arp.h>
-#include <stdlib.h>
-#include <sys/wait.h>
-
-#include "bpf/BpfUtils.h"
-#include "bpf_shared.h"
-
-namespace android {
-namespace net {
-
-class TcUtilsTest : public ::testing::Test {
-  public:
-    void SetUp() {}
-};
-
-TEST_F(TcUtilsTest, HardwareAddressTypeOfNonExistingIf) {
-    ASSERT_EQ(-ENODEV, hardwareAddressType("not_existing_if"));
-}
-
-TEST_F(TcUtilsTest, HardwareAddressTypeOfLoopback) {
-    ASSERT_EQ(ARPHRD_LOOPBACK, hardwareAddressType("lo"));
-}
-
-// If wireless 'wlan0' interface exists it should be Ethernet.
-TEST_F(TcUtilsTest, HardwareAddressTypeOfWireless) {
-    int type = hardwareAddressType("wlan0");
-    if (type == -ENODEV) return;
-
-    ASSERT_EQ(ARPHRD_ETHER, type);
-}
-
-// If cellular 'rmnet_data0' interface exists it should
-// *probably* not be Ethernet and instead be RawIp.
-TEST_F(TcUtilsTest, HardwareAddressTypeOfCellular) {
-    int type = hardwareAddressType("rmnet_data0");
-    if (type == -ENODEV) return;
-
-    ASSERT_NE(ARPHRD_ETHER, type);
-
-    // ARPHRD_RAWIP is 530 on some pre-4.14 Qualcomm devices.
-    if (type == 530) return;
-
-    ASSERT_EQ(ARPHRD_RAWIP, type);
-}
-
-TEST_F(TcUtilsTest, IsEthernetOfNonExistingIf) {
-    auto res = isEthernet("not_existing_if");
-    ASSERT_FALSE(res.ok());
-    ASSERT_EQ(ENODEV, res.error().code());
-}
-
-TEST_F(TcUtilsTest, IsEthernetOfLoopback) {
-    auto res = isEthernet("lo");
-    ASSERT_FALSE(res.ok());
-    ASSERT_EQ(EAFNOSUPPORT, res.error().code());
-}
-
-// If wireless 'wlan0' interface exists it should be Ethernet.
-// See also HardwareAddressTypeOfWireless.
-TEST_F(TcUtilsTest, IsEthernetOfWireless) {
-    auto res = isEthernet("wlan0");
-    if (!res.ok() && res.error().code() == ENODEV) return;
-
-    ASSERT_RESULT_OK(res);
-    ASSERT_TRUE(res.value());
-}
-
-// If cellular 'rmnet_data0' interface exists it should
-// *probably* not be Ethernet and instead be RawIp.
-// See also HardwareAddressTypeOfCellular.
-TEST_F(TcUtilsTest, IsEthernetOfCellular) {
-    auto res = isEthernet("rmnet_data0");
-    if (!res.ok() && res.error().code() == ENODEV) return;
-
-    ASSERT_RESULT_OK(res);
-    ASSERT_FALSE(res.value());
-}
-
-TEST_F(TcUtilsTest, DeviceMTUOfNonExistingIf) {
-    ASSERT_EQ(-ENODEV, deviceMTU("not_existing_if"));
-}
-
-TEST_F(TcUtilsTest, DeviceMTUofLoopback) {
-    ASSERT_EQ(65536, deviceMTU("lo"));
-}
-
-TEST_F(TcUtilsTest, GetClatEgress4MapFd) {
-    int fd = getClatEgress4MapFd();
-    ASSERT_GE(fd, 3);  // 0,1,2 - stdin/out/err, thus fd >= 3
-    EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD));
-    close(fd);
-}
-
-TEST_F(TcUtilsTest, GetClatEgress4RawIpProgFd) {
-    int fd = getClatEgress4ProgFd(RAWIP);
-    ASSERT_GE(fd, 3);
-    EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD));
-    close(fd);
-}
-
-TEST_F(TcUtilsTest, GetClatEgress4EtherProgFd) {
-    int fd = getClatEgress4ProgFd(ETHER);
-    ASSERT_GE(fd, 3);
-    EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD));
-    close(fd);
-}
-
-TEST_F(TcUtilsTest, GetClatIngress6MapFd) {
-    int fd = getClatIngress6MapFd();
-    ASSERT_GE(fd, 3);  // 0,1,2 - stdin/out/err, thus fd >= 3
-    EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD));
-    close(fd);
-}
-
-TEST_F(TcUtilsTest, GetClatIngress6RawIpProgFd) {
-    int fd = getClatIngress6ProgFd(RAWIP);
-    ASSERT_GE(fd, 3);
-    EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD));
-    close(fd);
-}
-
-TEST_F(TcUtilsTest, GetClatIngress6EtherProgFd) {
-    int fd = getClatIngress6ProgFd(ETHER);
-    ASSERT_GE(fd, 3);
-    EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD));
-    close(fd);
-}
-
-// See Linux kernel source in include/net/flow.h
-#define LOOPBACK_IFINDEX 1
-
-TEST_F(TcUtilsTest, AttachReplaceDetachClsactLo) {
-    // This attaches and detaches a configuration-less and thus no-op clsact
-    // qdisc to loopback interface (and it takes fractions of a second)
-    EXPECT_EQ(0, tcQdiscAddDevClsact(LOOPBACK_IFINDEX));
-    EXPECT_EQ(0, tcQdiscReplaceDevClsact(LOOPBACK_IFINDEX));
-    EXPECT_EQ(0, tcQdiscDelDevClsact(LOOPBACK_IFINDEX));
-    EXPECT_EQ(-EINVAL, tcQdiscDelDevClsact(LOOPBACK_IFINDEX));
-}
-
-static void checkAttachDetachBpfFilterClsactLo(const bool ingress, const bool ethernet) {
-    // Older kernels return EINVAL instead of ENOENT due to lacking proper error propagation...
-    const int errNOENT = android::bpf::isAtLeastKernelVersion(4, 19, 0) ? ENOENT : EINVAL;
-
-    int clatBpfFd = ingress ? getClatIngress6ProgFd(ethernet) : getClatEgress4ProgFd(ethernet);
-    ASSERT_GE(clatBpfFd, 3);
-
-    // This attaches and detaches a clsact plus ebpf program to loopback
-    // interface, but it should not affect traffic by virtue of us not
-    // actually populating the ebpf control map.
-    // Furthermore: it only takes fractions of a second.
-    EXPECT_EQ(-EINVAL, tcFilterDelDevIngressClatIpv6(LOOPBACK_IFINDEX));
-    EXPECT_EQ(-EINVAL, tcFilterDelDevEgressClatIpv4(LOOPBACK_IFINDEX));
-    EXPECT_EQ(0, tcQdiscAddDevClsact(LOOPBACK_IFINDEX));
-    EXPECT_EQ(-errNOENT, tcFilterDelDevIngressClatIpv6(LOOPBACK_IFINDEX));
-    EXPECT_EQ(-errNOENT, tcFilterDelDevEgressClatIpv4(LOOPBACK_IFINDEX));
-    if (ingress) {
-        EXPECT_EQ(0, tcFilterAddDevIngressClatIpv6(LOOPBACK_IFINDEX, clatBpfFd, ethernet));
-        EXPECT_EQ(0, tcFilterDelDevIngressClatIpv6(LOOPBACK_IFINDEX));
-    } else {
-        EXPECT_EQ(0, tcFilterAddDevEgressClatIpv4(LOOPBACK_IFINDEX, clatBpfFd, ethernet));
-        EXPECT_EQ(0, tcFilterDelDevEgressClatIpv4(LOOPBACK_IFINDEX));
-    }
-    EXPECT_EQ(-errNOENT, tcFilterDelDevIngressClatIpv6(LOOPBACK_IFINDEX));
-    EXPECT_EQ(-errNOENT, tcFilterDelDevEgressClatIpv4(LOOPBACK_IFINDEX));
-    EXPECT_EQ(0, tcQdiscDelDevClsact(LOOPBACK_IFINDEX));
-    EXPECT_EQ(-EINVAL, tcFilterDelDevIngressClatIpv6(LOOPBACK_IFINDEX));
-    EXPECT_EQ(-EINVAL, tcFilterDelDevEgressClatIpv4(LOOPBACK_IFINDEX));
-
-    close(clatBpfFd);
-}
-
-TEST_F(TcUtilsTest, CheckAttachBpfFilterRawIpClsactEgressLo) {
-    checkAttachDetachBpfFilterClsactLo(EGRESS, RAWIP);
-}
-
-TEST_F(TcUtilsTest, CheckAttachBpfFilterEthernetClsactEgressLo) {
-    checkAttachDetachBpfFilterClsactLo(EGRESS, ETHER);
-}
-
-TEST_F(TcUtilsTest, CheckAttachBpfFilterRawIpClsactIngressLo) {
-    checkAttachDetachBpfFilterClsactLo(INGRESS, RAWIP);
-}
-
-TEST_F(TcUtilsTest, CheckAttachBpfFilterEthernetClsactIngressLo) {
-    checkAttachDetachBpfFilterClsactLo(INGRESS, ETHER);
-}
-
-}  // namespace net
-}  // namespace android
diff --git a/service/native/libs/libclat/bpfhelper.cpp b/service/native/libs/libclat/bpfhelper.cpp
deleted file mode 100644
index 00785ad..0000000
--- a/service/native/libs/libclat/bpfhelper.cpp
+++ /dev/null
@@ -1,221 +0,0 @@
-/*
- * Copyright 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.
- *
- * main.c - main function
- */
-#define LOG_TAG "bpfhelper"
-
-#include "libclat/bpfhelper.h"
-
-#include <android-base/unique_fd.h>
-#include <log/log.h>
-
-#include "bpf/BpfMap.h"
-#include "libclat/TcUtils.h"
-
-#define DEVICEPREFIX "v4-"
-
-using android::base::unique_fd;
-using android::bpf::BpfMap;
-
-BpfMap<ClatEgress4Key, ClatEgress4Value> mClatEgress4Map;
-BpfMap<ClatIngress6Key, ClatIngress6Value> mClatIngress6Map;
-
-namespace android {
-namespace net {
-namespace clat {
-
-// TODO: have a clearMap function to remove all stubs while system server crash.
-// For long term, move bpf access into java and map initialization should live
-// ClatCoordinator constructor.
-int initMaps(void) {
-    int rv = getClatEgress4MapFd();
-    if (rv < 0) {
-        ALOGE("getClatEgress4MapFd() failure: %s", strerror(-rv));
-        return -rv;
-    }
-    mClatEgress4Map.reset(rv);
-
-    rv = getClatIngress6MapFd();
-    if (rv < 0) {
-        ALOGE("getClatIngress6MapFd() failure: %s", strerror(-rv));
-        mClatEgress4Map.reset(-1);
-        return -rv;
-    }
-    mClatIngress6Map.reset(rv);
-
-    return 0;
-}
-
-void maybeStartBpf(const ClatdTracker& tracker) {
-    auto isEthernet = android::net::isEthernet(tracker.iface);
-    if (!isEthernet.ok()) {
-        ALOGE("isEthernet(%s[%d]) failure: %s", tracker.iface, tracker.ifIndex,
-              isEthernet.error().message().c_str());
-        return;
-    }
-
-    // This program will be attached to the v4-* interface which is a TUN and thus always rawip.
-    int rv = getClatEgress4ProgFd(RAWIP);
-    if (rv < 0) {
-        ALOGE("getClatEgress4ProgFd(RAWIP) failure: %s", strerror(-rv));
-        return;
-    }
-    unique_fd txRawIpProgFd(rv);
-
-    rv = getClatIngress6ProgFd(isEthernet.value());
-    if (rv < 0) {
-        ALOGE("getClatIngress6ProgFd(%d) failure: %s", isEthernet.value(), strerror(-rv));
-        return;
-    }
-    unique_fd rxProgFd(rv);
-
-    ClatEgress4Key txKey = {
-            .iif = tracker.v4ifIndex,
-            .local4 = tracker.v4,
-    };
-    ClatEgress4Value txValue = {
-            .oif = tracker.ifIndex,
-            .local6 = tracker.v6,
-            .pfx96 = tracker.pfx96,
-            .oifIsEthernet = isEthernet.value(),
-    };
-
-    auto ret = mClatEgress4Map.writeValue(txKey, txValue, BPF_ANY);
-    if (!ret.ok()) {
-        ALOGE("mClatEgress4Map.writeValue failure: %s", strerror(ret.error().code()));
-        return;
-    }
-
-    ClatIngress6Key rxKey = {
-            .iif = tracker.ifIndex,
-            .pfx96 = tracker.pfx96,
-            .local6 = tracker.v6,
-    };
-    ClatIngress6Value rxValue = {
-            // TODO: move all the clat code to eBPF and remove the tun interface entirely.
-            .oif = tracker.v4ifIndex,
-            .local4 = tracker.v4,
-    };
-
-    ret = mClatIngress6Map.writeValue(rxKey, rxValue, BPF_ANY);
-    if (!ret.ok()) {
-        ALOGE("mClatIngress6Map.writeValue failure: %s", strerror(ret.error().code()));
-        ret = mClatEgress4Map.deleteValue(txKey);
-        if (!ret.ok())
-            ALOGE("mClatEgress4Map.deleteValue failure: %s", strerror(ret.error().code()));
-        return;
-    }
-
-    // We do tc setup *after* populating the maps, so scanning through them
-    // can always be used to tell us what needs cleanup.
-
-    // Usually the clsact will be added in RouteController::addInterfaceToPhysicalNetwork.
-    // But clat is started before the v4- interface is added to the network. The clat startup have
-    // to add clsact of v4- tun interface first for adding bpf filter in maybeStartBpf.
-    // TODO: move "qdisc add clsact" of v4- tun interface out from ClatdController.
-    rv = tcQdiscAddDevClsact(tracker.v4ifIndex);
-    if (rv) {
-        ALOGE("tcQdiscAddDevClsact(%d[%s]) failure: %s", tracker.v4ifIndex, tracker.v4iface,
-              strerror(-rv));
-        ret = mClatEgress4Map.deleteValue(txKey);
-        if (!ret.ok())
-            ALOGE("mClatEgress4Map.deleteValue failure: %s", strerror(ret.error().code()));
-        ret = mClatIngress6Map.deleteValue(rxKey);
-        if (!ret.ok())
-            ALOGE("mClatIngress6Map.deleteValue failure: %s", strerror(ret.error().code()));
-        return;
-    }
-
-    rv = tcFilterAddDevEgressClatIpv4(tracker.v4ifIndex, txRawIpProgFd, RAWIP);
-    if (rv) {
-        ALOGE("tcFilterAddDevEgressClatIpv4(%d[%s], RAWIP) failure: %s", tracker.v4ifIndex,
-              tracker.v4iface, strerror(-rv));
-
-        // The v4- interface clsact is not deleted for unwinding error because once it is created
-        // with interface addition, the lifetime is till interface deletion. Moreover, the clsact
-        // has no clat filter now. It should not break anything.
-
-        ret = mClatEgress4Map.deleteValue(txKey);
-        if (!ret.ok())
-            ALOGE("mClatEgress4Map.deleteValue failure: %s", strerror(ret.error().code()));
-        ret = mClatIngress6Map.deleteValue(rxKey);
-        if (!ret.ok())
-            ALOGE("mClatIngress6Map.deleteValue failure: %s", strerror(ret.error().code()));
-        return;
-    }
-
-    rv = tcFilterAddDevIngressClatIpv6(tracker.ifIndex, rxProgFd, isEthernet.value());
-    if (rv) {
-        ALOGE("tcFilterAddDevIngressClatIpv6(%d[%s], %d) failure: %s", tracker.ifIndex,
-              tracker.iface, isEthernet.value(), strerror(-rv));
-        rv = tcFilterDelDevEgressClatIpv4(tracker.v4ifIndex);
-        if (rv) {
-            ALOGE("tcFilterDelDevEgressClatIpv4(%d[%s]) failure: %s", tracker.v4ifIndex,
-                  tracker.v4iface, strerror(-rv));
-        }
-
-        // The v4- interface clsact is not deleted. See the reason in the error unwinding code of
-        // the egress filter attaching of v4- tun interface.
-
-        ret = mClatEgress4Map.deleteValue(txKey);
-        if (!ret.ok())
-            ALOGE("mClatEgress4Map.deleteValue failure: %s", strerror(ret.error().code()));
-        ret = mClatIngress6Map.deleteValue(rxKey);
-        if (!ret.ok())
-            ALOGE("mClatIngress6Map.deleteValue failure: %s", strerror(ret.error().code()));
-        return;
-    }
-
-    // success
-}
-
-void maybeStopBpf(const ClatdTracker& tracker) {
-    int rv = tcFilterDelDevIngressClatIpv6(tracker.ifIndex);
-    if (rv < 0) {
-        ALOGE("tcFilterDelDevIngressClatIpv6(%d[%s]) failure: %s", tracker.ifIndex, tracker.iface,
-              strerror(-rv));
-    }
-
-    rv = tcFilterDelDevEgressClatIpv4(tracker.v4ifIndex);
-    if (rv < 0) {
-        ALOGE("tcFilterDelDevEgressClatIpv4(%d[%s]) failure: %s", tracker.v4ifIndex,
-              tracker.v4iface, strerror(-rv));
-    }
-
-    // We cleanup the maps last, so scanning through them can be used to
-    // determine what still needs cleanup.
-
-    ClatEgress4Key txKey = {
-            .iif = tracker.v4ifIndex,
-            .local4 = tracker.v4,
-    };
-
-    auto ret = mClatEgress4Map.deleteValue(txKey);
-    if (!ret.ok()) ALOGE("mClatEgress4Map.deleteValue failure: %s", strerror(ret.error().code()));
-
-    ClatIngress6Key rxKey = {
-            .iif = tracker.ifIndex,
-            .pfx96 = tracker.pfx96,
-            .local6 = tracker.v6,
-    };
-
-    ret = mClatIngress6Map.deleteValue(rxKey);
-    if (!ret.ok()) ALOGE("mClatIngress6Map.deleteValue failure: %s", strerror(ret.error().code()));
-}
-
-}  // namespace clat
-}  // namespace net
-}  // namespace android
diff --git a/service/native/libs/libclat/include/libclat/TcUtils.h b/service/native/libs/libclat/include/libclat/TcUtils.h
deleted file mode 100644
index 212838e..0000000
--- a/service/native/libs/libclat/include/libclat/TcUtils.h
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#pragma once
-
-#include <android-base/result.h>
-#include <errno.h>
-#include <linux/if_ether.h>
-#include <linux/if_link.h>
-#include <linux/rtnetlink.h>
-
-#include <string>
-
-#include "bpf/BpfUtils.h"
-#include "bpf_shared.h"
-
-namespace android {
-namespace net {
-
-// For better code clarity - do not change values - used for booleans like
-// with_ethernet_header or isEthernet.
-constexpr bool RAWIP = false;
-constexpr bool ETHER = true;
-
-// For better code clarity when used for 'bool ingress' parameter.
-constexpr bool EGRESS = false;
-constexpr bool INGRESS = true;
-
-// The priority of clat hook - must be after tethering.
-constexpr uint16_t PRIO_CLAT = 4;
-
-// this returns an ARPHRD_* constant or a -errno
-int hardwareAddressType(const std::string& interface);
-
-// return MTU or -errno
-int deviceMTU(const std::string& interface);
-
-base::Result<bool> isEthernet(const std::string& interface);
-
-inline int getClatEgress4MapFd(void) {
-    const int fd = bpf::mapRetrieveRW(CLAT_EGRESS4_MAP_PATH);
-    return (fd == -1) ? -errno : fd;
-}
-
-inline int getClatEgress4ProgFd(bool with_ethernet_header) {
-    const int fd = bpf::retrieveProgram(with_ethernet_header ? CLAT_EGRESS4_PROG_ETHER_PATH
-                                                             : CLAT_EGRESS4_PROG_RAWIP_PATH);
-    return (fd == -1) ? -errno : fd;
-}
-
-inline int getClatIngress6MapFd(void) {
-    const int fd = bpf::mapRetrieveRW(CLAT_INGRESS6_MAP_PATH);
-    return (fd == -1) ? -errno : fd;
-}
-
-inline int getClatIngress6ProgFd(bool with_ethernet_header) {
-    const int fd = bpf::retrieveProgram(with_ethernet_header ? CLAT_INGRESS6_PROG_ETHER_PATH
-                                                             : CLAT_INGRESS6_PROG_RAWIP_PATH);
-    return (fd == -1) ? -errno : fd;
-}
-
-int doTcQdiscClsact(int ifIndex, uint16_t nlMsgType, uint16_t nlMsgFlags);
-
-inline int tcQdiscAddDevClsact(int ifIndex) {
-    return doTcQdiscClsact(ifIndex, RTM_NEWQDISC, NLM_F_EXCL | NLM_F_CREATE);
-}
-
-inline int tcQdiscReplaceDevClsact(int ifIndex) {
-    return doTcQdiscClsact(ifIndex, RTM_NEWQDISC, NLM_F_CREATE | NLM_F_REPLACE);
-}
-
-inline int tcQdiscDelDevClsact(int ifIndex) {
-    return doTcQdiscClsact(ifIndex, RTM_DELQDISC, 0);
-}
-
-// tc filter add dev .. in/egress prio 4 protocol ipv6/ip bpf object-pinned /sys/fs/bpf/...
-// direct-action
-int tcFilterAddDevBpf(int ifIndex, bool ingress, uint16_t proto, int bpfFd, bool ethernet);
-
-// tc filter add dev .. ingress prio 4 protocol ipv6 bpf object-pinned /sys/fs/bpf/... direct-action
-inline int tcFilterAddDevIngressClatIpv6(int ifIndex, int bpfFd, bool ethernet) {
-    return tcFilterAddDevBpf(ifIndex, INGRESS, ETH_P_IPV6, bpfFd, ethernet);
-}
-
-// tc filter add dev .. egress prio 4 protocol ip bpf object-pinned /sys/fs/bpf/... direct-action
-inline int tcFilterAddDevEgressClatIpv4(int ifIndex, int bpfFd, bool ethernet) {
-    return tcFilterAddDevBpf(ifIndex, EGRESS, ETH_P_IP, bpfFd, ethernet);
-}
-
-// tc filter del dev .. in/egress prio .. protocol ..
-int tcFilterDelDev(int ifIndex, bool ingress, uint16_t prio, uint16_t proto);
-
-// tc filter del dev .. ingress prio 4 protocol ipv6
-inline int tcFilterDelDevIngressClatIpv6(int ifIndex) {
-    return tcFilterDelDev(ifIndex, INGRESS, PRIO_CLAT, ETH_P_IPV6);
-}
-
-// tc filter del dev .. egress prio 4 protocol ip
-inline int tcFilterDelDevEgressClatIpv4(int ifIndex) {
-    return tcFilterDelDev(ifIndex, EGRESS, PRIO_CLAT, ETH_P_IP);
-}
-
-}  // namespace net
-}  // namespace android
diff --git a/service/native/libs/libclat/include/libclat/bpfhelper.h b/service/native/libs/libclat/include/libclat/bpfhelper.h
deleted file mode 100644
index c0328c0..0000000
--- a/service/native/libs/libclat/include/libclat/bpfhelper.h
+++ /dev/null
@@ -1,40 +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.
-
-#pragma once
-
-#include <arpa/inet.h>
-#include <linux/if.h>
-
-namespace android {
-namespace net {
-namespace clat {
-
-struct ClatdTracker {
-    unsigned ifIndex;
-    char iface[IFNAMSIZ];
-    unsigned v4ifIndex;
-    char v4iface[IFNAMSIZ];
-    in_addr v4;
-    in6_addr v6;
-    in6_addr pfx96;
-};
-
-int initMaps(void);
-void maybeStartBpf(const ClatdTracker& tracker);
-void maybeStopBpf(const ClatdTracker& tracker);
-
-}  // namespace clat
-}  // namespace net
-}  // namespace android
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 0528f29..4835438 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -2864,12 +2864,16 @@
     }
 
     private void enforceNetworkFactoryPermission() {
+        // TODO: Check for the BLUETOOTH_STACK permission once that is in the API surface.
+        if (getCallingUid() == Process.BLUETOOTH_UID) return;
         enforceAnyPermissionOf(
                 android.Manifest.permission.NETWORK_FACTORY,
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
     }
 
     private void enforceNetworkFactoryOrSettingsPermission() {
+        // TODO: Check for the BLUETOOTH_STACK permission once that is in the API surface.
+        if (getCallingUid() == Process.BLUETOOTH_UID) return;
         enforceAnyPermissionOf(
                 android.Manifest.permission.NETWORK_SETTINGS,
                 android.Manifest.permission.NETWORK_FACTORY,
@@ -2877,6 +2881,8 @@
     }
 
     private void enforceNetworkFactoryOrTestNetworksPermission() {
+        // TODO: Check for the BLUETOOTH_STACK permission once that is in the API surface.
+        if (getCallingUid() == Process.BLUETOOTH_UID) return;
         enforceAnyPermissionOf(
                 android.Manifest.permission.MANAGE_TEST_NETWORKS,
                 android.Manifest.permission.NETWORK_FACTORY,
@@ -2889,7 +2895,8 @@
                 || PERMISSION_GRANTED == mContext.checkPermission(
                 android.Manifest.permission.NETWORK_SETTINGS, pid, uid)
                 || PERMISSION_GRANTED == mContext.checkPermission(
-                NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, pid, uid);
+                NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, pid, uid)
+                || uid == Process.BLUETOOTH_UID;
     }
 
     private boolean checkSettingsPermission() {
@@ -3259,11 +3266,12 @@
             return;
         }
 
-        pw.print("NetworkProviders for:");
+        pw.println("NetworkProviders for:");
+        pw.increaseIndent();
         for (NetworkProviderInfo npi : mNetworkProviderInfos.values()) {
-            pw.print(" " + npi.name);
+            pw.println(npi.providerId + ": " + npi.name);
         }
-        pw.println();
+        pw.decreaseIndent();
         pw.println();
 
         final NetworkAgentInfo defaultNai = getDefaultNetwork();
@@ -3312,6 +3320,14 @@
         pw.decreaseIndent();
         pw.println();
 
+        pw.println("Network Offers:");
+        pw.increaseIndent();
+        for (final NetworkOfferInfo offerInfo : mNetworkOffers) {
+            pw.println(offerInfo.offer);
+        }
+        pw.decreaseIndent();
+        pw.println();
+
         mLegacyTypeTracker.dump(pw);
 
         pw.println();
@@ -3662,7 +3678,7 @@
                 }
                 case NetworkAgent.EVENT_REMOVE_ALL_DSCP_POLICIES: {
                     if (mDscpPolicyTracker != null) {
-                        mDscpPolicyTracker.removeAllDscpPolicies(nai);
+                        mDscpPolicyTracker.removeAllDscpPolicies(nai, true);
                     }
                     break;
                 }
@@ -4403,6 +4419,9 @@
     }
 
     private void destroyNativeNetwork(@NonNull NetworkAgentInfo nai) {
+        if (mDscpPolicyTracker != null) {
+            mDscpPolicyTracker.removeAllDscpPolicies(nai, false);
+        }
         try {
             mNetd.networkDestroy(nai.network.getNetId());
         } catch (RemoteException | ServiceSpecificException e) {
diff --git a/service/src/com/android/server/TestNetworkService.java b/service/src/com/android/server/TestNetworkService.java
index a0bfb4a..ccc2776 100644
--- a/service/src/com/android/server/TestNetworkService.java
+++ b/service/src/com/android/server/TestNetworkService.java
@@ -231,6 +231,7 @@
         nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED);
         nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
         nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
+        nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN);
         nc.setNetworkSpecifier(new TestNetworkSpecifier(iface));
         nc.setAdministratorUids(administratorUids);
         if (!isMetered) {
diff --git a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
index ce955fd..b06c8aa 100644
--- a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
+++ b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
@@ -29,7 +29,6 @@
 import android.net.NetworkCapabilities;
 import android.net.NetworkSpecifier;
 import android.net.TelephonyNetworkSpecifier;
-import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Process;
@@ -94,11 +93,7 @@
             @NonNull final TelephonyManager t) {
         mContext = c;
         mTelephonyManager = t;
-        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S) {
-            mTelephonyManagerShim = new TelephonyManagerShimImpl(mTelephonyManager);
-        } else {
-            mTelephonyManagerShim = null;
-        }
+        mTelephonyManagerShim = TelephonyManagerShimImpl.newInstance(mTelephonyManager);
         mThread = new HandlerThread(TAG);
         mThread.start();
         mHandler = new Handler(mThread.getLooper()) {};
@@ -192,36 +187,30 @@
 
     private void addCarrierPrivilegesListener(int logicalSlotIndex, Executor executor,
             CarrierPrivilegesListenerShim listener) {
-        if (mTelephonyManagerShim  == null) {
-            return;
-        }
         try {
             mTelephonyManagerShim.addCarrierPrivilegesListener(
                     logicalSlotIndex, executor, listener);
         } catch (UnsupportedApiLevelException unsupportedApiLevelException) {
+            // Should not happen since CarrierPrivilegeAuthenticator is only used on T+
             Log.e(TAG, "addCarrierPrivilegesListener API is not available");
         }
     }
 
     private void removeCarrierPrivilegesListener(CarrierPrivilegesListenerShim listener) {
-        if (mTelephonyManagerShim  == null) {
-            return;
-        }
         try {
             mTelephonyManagerShim.removeCarrierPrivilegesListener(listener);
         } catch (UnsupportedApiLevelException unsupportedApiLevelException) {
+            // Should not happen since CarrierPrivilegeAuthenticator is only used on T+
             Log.e(TAG, "removeCarrierPrivilegesListener API is not available");
         }
     }
 
     private String getCarrierServicePackageNameForLogicalSlot(int logicalSlotIndex) {
-        if (mTelephonyManagerShim  == null) {
-            return null;
-        }
         try {
             return mTelephonyManagerShim.getCarrierServicePackageNameForLogicalSlot(
                     logicalSlotIndex);
         } catch (UnsupportedApiLevelException unsupportedApiLevelException) {
+            // Should not happen since CarrierPrivilegeAuthenticator is only used on T+
             Log.e(TAG, "getCarrierServicePackageNameForLogicalSlot API is not available");
         }
         return null;
diff --git a/service/src/com/android/server/connectivity/ClatCoordinator.java b/service/src/com/android/server/connectivity/ClatCoordinator.java
index 2e26ae4..8aa5990 100644
--- a/service/src/com/android/server/connectivity/ClatCoordinator.java
+++ b/service/src/com/android/server/connectivity/ClatCoordinator.java
@@ -18,6 +18,8 @@
 
 import static android.net.INetd.IF_STATE_UP;
 import static android.net.INetd.PERMISSION_SYSTEM;
+import static android.system.OsConstants.ETH_P_IP;
+import static android.system.OsConstants.ETH_P_IPV6;
 
 import static com.android.net.module.util.NetworkStackConstants.IPV6_MIN_MTU;
 
@@ -30,10 +32,19 @@
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
+import android.system.ErrnoException;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.BpfMap;
+import com.android.net.module.util.IBpfMap;
 import com.android.net.module.util.InterfaceParams;
+import com.android.net.module.util.TcUtils;
+import com.android.net.module.util.bpf.ClatEgress4Key;
+import com.android.net.module.util.bpf.ClatEgress4Value;
+import com.android.net.module.util.bpf.ClatIngress6Key;
+import com.android.net.module.util.bpf.ClatIngress6Value;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
@@ -71,11 +82,44 @@
 
     private static final int INVALID_IFINDEX = 0;
 
+    // For better code clarity when used for 'bool ingress' parameter.
+    @VisibleForTesting
+    static final boolean EGRESS = false;
+    @VisibleForTesting
+    static final boolean INGRESS = true;
+
+    // For better code clarity when used for 'bool ether' parameter.
+    static final boolean RAWIP = false;
+    static final boolean ETHER = true;
+
+    // The priority of clat hook - must be after tethering.
+    @VisibleForTesting
+    static final int PRIO_CLAT = 4;
+
+    private static final String CLAT_EGRESS4_MAP_PATH = makeMapPath("egress4");
+    private static final String CLAT_INGRESS6_MAP_PATH = makeMapPath("ingress6");
+
+    private static String makeMapPath(String which) {
+        return "/sys/fs/bpf/map_clatd_clat_" + which + "_map";
+    }
+
+    private static String makeProgPath(boolean ingress, boolean ether) {
+        String path = "/sys/fs/bpf/prog_clatd_schedcls_"
+                + (ingress ? "ingress6" : "egress4")
+                + "_clat_"
+                + (ether ? "ether" : "rawip");
+        return path;
+    }
+
     @NonNull
     private final INetd mNetd;
     @NonNull
     private final Dependencies mDeps;
     @Nullable
+    private final IBpfMap<ClatIngress6Key, ClatIngress6Value> mIngressMap;
+    @Nullable
+    private final IBpfMap<ClatEgress4Key, ClatEgress4Value> mEgressMap;
+    @Nullable
     private ClatdTracker mClatdTracker = null;
 
     @VisibleForTesting
@@ -195,6 +239,62 @@
         public void untagSocket(long cookie) throws IOException {
             native_untagSocket(cookie);
         }
+
+        /** Get ingress6 BPF map. */
+        @Nullable
+        public IBpfMap<ClatIngress6Key, ClatIngress6Value> getBpfIngress6Map() {
+            // Pre-T devices don't use ClatCoordinator to access clat map. Since Nat464Xlat
+            // initializes a ClatCoordinator object to avoid redundant null pointer check
+            // while using, ignore the BPF map initialization on pre-T devices.
+            // TODO: probably don't initialize ClatCoordinator object on pre-T devices.
+            if (!SdkLevel.isAtLeastT()) return null;
+            try {
+                return new BpfMap<>(CLAT_INGRESS6_MAP_PATH,
+                    BpfMap.BPF_F_RDWR, ClatIngress6Key.class, ClatIngress6Value.class);
+            } catch (ErrnoException e) {
+                Log.e(TAG, "Cannot create ingress6 map: " + e);
+                return null;
+            }
+        }
+
+        /** Get egress4 BPF map. */
+        @Nullable
+        public IBpfMap<ClatEgress4Key, ClatEgress4Value> getBpfEgress4Map() {
+            // Pre-T devices don't use ClatCoordinator to access clat map. Since Nat464Xlat
+            // initializes a ClatCoordinator object to avoid redundant null pointer check
+            // while using, ignore the BPF map initialization on pre-T devices.
+            // TODO: probably don't initialize ClatCoordinator object on pre-T devices.
+            if (!SdkLevel.isAtLeastT()) return null;
+            try {
+                return new BpfMap<>(CLAT_EGRESS4_MAP_PATH,
+                    BpfMap.BPF_F_RDWR, ClatEgress4Key.class, ClatEgress4Value.class);
+            } catch (ErrnoException e) {
+                Log.e(TAG, "Cannot create egress4 map: " + e);
+                return null;
+            }
+        }
+
+        /** Checks if the network interface uses an ethernet L2 header. */
+        public boolean isEthernet(String iface) throws IOException {
+            return TcUtils.isEthernet(iface);
+        }
+
+        /** Add a clsact qdisc. */
+        public void tcQdiscAddDevClsact(int ifIndex) throws IOException {
+            TcUtils.tcQdiscAddDevClsact(ifIndex);
+        }
+
+        /** Attach a tc bpf filter. */
+        public void tcFilterAddDevBpf(int ifIndex, boolean ingress, short prio, short proto,
+                String bpfProgPath) throws IOException {
+            TcUtils.tcFilterAddDevBpf(ifIndex, ingress, prio, proto, bpfProgPath);
+        }
+
+        /** Delete a tc filter. */
+        public void tcFilterDelDev(int ifIndex, boolean ingress, short prio, short proto)
+                throws IOException {
+            TcUtils.tcFilterDelDev(ifIndex, ingress, prio, proto);
+        }
     }
 
     @VisibleForTesting
@@ -268,6 +368,129 @@
     public ClatCoordinator(@NonNull Dependencies deps) {
         mDeps = deps;
         mNetd = mDeps.getNetd();
+        mIngressMap = mDeps.getBpfIngress6Map();
+        mEgressMap = mDeps.getBpfEgress4Map();
+    }
+
+    private void maybeStartBpf(final ClatdTracker tracker) {
+        if (mIngressMap == null || mEgressMap == null) return;
+
+        final boolean isEthernet;
+        try {
+            isEthernet = mDeps.isEthernet(tracker.iface);
+        } catch (IOException e) {
+            Log.e(TAG, "Fail to call isEthernet for interface " + tracker.iface);
+            return;
+        }
+
+        final ClatEgress4Key txKey = new ClatEgress4Key(tracker.v4ifIndex, tracker.v4);
+        final ClatEgress4Value txValue = new ClatEgress4Value(tracker.ifIndex, tracker.v6,
+                tracker.pfx96, (short) (isEthernet ? 1 /* ETHER */ : 0 /* RAWIP */));
+        try {
+            mEgressMap.insertEntry(txKey, txValue);
+        } catch (ErrnoException | IllegalStateException e) {
+            Log.e(TAG, "Could not insert entry (" + txKey + ", " + txValue + ") on egress map: "
+                    + e);
+            return;
+        }
+
+        final ClatIngress6Key rxKey = new ClatIngress6Key(tracker.ifIndex, tracker.pfx96,
+                tracker.v6);
+        final ClatIngress6Value rxValue = new ClatIngress6Value(tracker.v4ifIndex,
+                tracker.v4);
+        try {
+            mIngressMap.insertEntry(rxKey, rxValue);
+        } catch (ErrnoException | IllegalStateException e) {
+            Log.e(TAG, "Could not insert entry (" + rxKey + ", " + rxValue + ") ingress map: "
+                    + e);
+            try {
+                mEgressMap.deleteEntry(txKey);
+            } catch (ErrnoException | IllegalStateException e2) {
+                Log.e(TAG, "Could not delete entry (" + txKey + ") from egress map: " + e2);
+            }
+            return;
+        }
+
+        // Usually the clsact will be added in netd RouteController::addInterfaceToPhysicalNetwork.
+        // But clat is started before the v4- interface is added to the network. The clat startup
+        // have to add clsact of v4- tun interface first for adding bpf filter in maybeStartBpf.
+        try {
+            // tc qdisc add dev .. clsact
+            mDeps.tcQdiscAddDevClsact(tracker.v4ifIndex);
+        } catch (IOException e) {
+            Log.e(TAG, "tc qdisc add dev (" + tracker.v4ifIndex + "[" + tracker.v4iface
+                    + "]) failure: " + e);
+            try {
+                mEgressMap.deleteEntry(txKey);
+            } catch (ErrnoException | IllegalStateException e2) {
+                Log.e(TAG, "Could not delete entry (" + txKey + ") from egress map: " + e2);
+            }
+            try {
+                mIngressMap.deleteEntry(rxKey);
+            } catch (ErrnoException | IllegalStateException e3) {
+                Log.e(TAG, "Could not delete entry (" + rxKey + ") from ingress map: " + e3);
+            }
+            return;
+        }
+
+        // This program will be attached to the v4-* interface which is a TUN and thus always rawip.
+        try {
+            // tc filter add dev .. egress prio 4 protocol ip bpf object-pinned /sys/fs/bpf/...
+            // direct-action
+            mDeps.tcFilterAddDevBpf(tracker.v4ifIndex, EGRESS, (short) PRIO_CLAT, (short) ETH_P_IP,
+                    makeProgPath(EGRESS, RAWIP));
+        } catch (IOException e) {
+            Log.e(TAG, "tc filter add dev (" + tracker.v4ifIndex + "[" + tracker.v4iface
+                    + "]) egress prio PRIO_CLAT protocol ip failure: " + e);
+
+            // The v4- interface clsact is not deleted for unwinding error because once it is
+            // created with interface addition, the lifetime is till interface deletion. Moreover,
+            // the clsact has no clat filter now. It should not break anything.
+
+            try {
+                mEgressMap.deleteEntry(txKey);
+            } catch (ErrnoException | IllegalStateException e2) {
+                Log.e(TAG, "Could not delete entry (" + txKey + ") from egress map: " + e2);
+            }
+            try {
+                mIngressMap.deleteEntry(rxKey);
+            } catch (ErrnoException | IllegalStateException e3) {
+                Log.e(TAG, "Could not delete entry (" + rxKey + ") from ingress map: " + e3);
+            }
+            return;
+        }
+
+        try {
+            // tc filter add dev .. ingress prio 4 protocol ipv6 bpf object-pinned /sys/fs/bpf/...
+            // direct-action
+            mDeps.tcFilterAddDevBpf(tracker.ifIndex, INGRESS, (short) PRIO_CLAT,
+                    (short) ETH_P_IPV6, makeProgPath(INGRESS, isEthernet));
+        } catch (IOException e) {
+            Log.e(TAG, "tc filter add dev (" + tracker.ifIndex + "[" + tracker.iface
+                    + "]) ingress prio PRIO_CLAT protocol ipv6 failure: " + e);
+
+            // The v4- interface clsact is not deleted. See the reason in the error unwinding code
+            // of the egress filter attaching of v4- tun interface.
+
+            try {
+                mDeps.tcFilterDelDev(tracker.v4ifIndex, EGRESS, (short) PRIO_CLAT,
+                        (short) ETH_P_IP);
+            } catch (IOException e2) {
+                Log.e(TAG, "tc filter del dev (" + tracker.v4ifIndex + "[" + tracker.v4iface
+                        + "]) egress prio PRIO_CLAT protocol ip failure: " + e2);
+            }
+            try {
+                mEgressMap.deleteEntry(txKey);
+            } catch (ErrnoException | IllegalStateException e3) {
+                Log.e(TAG, "Could not delete entry (" + txKey + ") from egress map: " + e3);
+            }
+            try {
+                mIngressMap.deleteEntry(rxKey);
+            } catch (ErrnoException | IllegalStateException e4) {
+                Log.e(TAG, "Could not delete entry (" + rxKey + ") from ingress map: " + e4);
+            }
+            return;
+        }
     }
 
     /**
@@ -454,9 +677,48 @@
         mClatdTracker = new ClatdTracker(iface, ifIndex, tunIface, tunIfIndex, v4, v6, pfx96,
                 pid, cookie);
 
+        // [7] Start BPF
+        maybeStartBpf(mClatdTracker);
+
         return v6Str;
     }
 
+    private void maybeStopBpf(final ClatdTracker tracker) {
+        if (mIngressMap == null || mEgressMap == null) return;
+
+        try {
+            mDeps.tcFilterDelDev(tracker.ifIndex, INGRESS, (short) PRIO_CLAT, (short) ETH_P_IPV6);
+        } catch (IOException e) {
+            Log.e(TAG, "tc filter del dev (" + tracker.ifIndex + "[" + tracker.iface
+                    + "]) ingress prio PRIO_CLAT protocol ipv6 failure: " + e);
+        }
+
+        try {
+            mDeps.tcFilterDelDev(tracker.v4ifIndex, EGRESS, (short) PRIO_CLAT, (short) ETH_P_IP);
+        } catch (IOException e) {
+            Log.e(TAG, "tc filter del dev (" + tracker.v4ifIndex + "[" + tracker.v4iface
+                    + "]) egress prio PRIO_CLAT protocol ip failure: " + e);
+        }
+
+        // We cleanup the maps last, so scanning through them can be used to
+        // determine what still needs cleanup.
+
+        final ClatEgress4Key txKey = new ClatEgress4Key(tracker.v4ifIndex, tracker.v4);
+        try {
+            mEgressMap.deleteEntry(txKey);
+        } catch (ErrnoException | IllegalStateException e) {
+            Log.e(TAG, "Could not delete entry (" + txKey + "): " + e);
+        }
+
+        final ClatIngress6Key rxKey = new ClatIngress6Key(tracker.ifIndex, tracker.pfx96,
+                tracker.v6);
+        try {
+            mIngressMap.deleteEntry(rxKey);
+        } catch (ErrnoException | IllegalStateException e) {
+            Log.e(TAG, "Could not delete entry (" + rxKey + "): " + e);
+        }
+    }
+
     /**
      * Stop clatd
      */
@@ -466,6 +728,7 @@
         }
         Log.i(TAG, "Stopping clatd pid=" + mClatdTracker.pid + " on " + mClatdTracker.iface);
 
+        maybeStopBpf(mClatdTracker);
         mDeps.stopClatd(mClatdTracker.iface, mClatdTracker.pfx96.getHostAddress(),
                 mClatdTracker.v4.getHostAddress(), mClatdTracker.v6.getHostAddress(),
                 mClatdTracker.pid);
diff --git a/service/src/com/android/server/connectivity/ConnectivityNativeService.java b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
new file mode 100644
index 0000000..cde6ea7
--- /dev/null
+++ b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
@@ -0,0 +1,176 @@
+/*
+ * 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.
+ */
+
+package com.android.server.connectivity;
+
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET4_BIND;
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET6_BIND;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.net.connectivity.aidl.ConnectivityNative;
+import android.os.Binder;
+import android.os.Process;
+import android.os.ServiceSpecificException;
+import android.system.ErrnoException;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.BpfBitmap;
+import com.android.net.module.util.BpfUtils;
+import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.PermissionUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * @hide
+ */
+public class ConnectivityNativeService extends ConnectivityNative.Stub {
+    public static final String SERVICE_NAME = "connectivity_native";
+
+    private static final String TAG = ConnectivityNativeService.class.getSimpleName();
+    private static final String CGROUP_PATH = "/sys/fs/cgroup";
+    private static final String V4_PROG_PATH =
+            "/sys/fs/bpf/prog_block_bind4_block_port";
+    private static final String V6_PROG_PATH =
+            "/sys/fs/bpf/prog_block_bind6_block_port";
+    private static final String BLOCKED_PORTS_MAP_PATH = "/sys/fs/bpf/map_block_blocked_ports_map";
+
+    private final Context mContext;
+
+    // BPF map for port blocking. Exactly 65536 entries long, with one entry per port number
+    @Nullable
+    private final BpfBitmap mBpfBlockedPortsMap;
+
+    /**
+     * Dependencies of ConnectivityNativeService, for injection in tests.
+     */
+    @VisibleForTesting
+    public static class Dependencies {
+        /** Get BPF maps. */
+        @Nullable public BpfBitmap getBlockPortsMap() {
+            try {
+                return new BpfBitmap(BLOCKED_PORTS_MAP_PATH);
+            } catch (ErrnoException e) {
+                throw new UnsupportedOperationException("Failed to create blocked ports map: "
+                        + e);
+            }
+        }
+    }
+
+    private void enforceBlockPortPermission() {
+        final int uid = Binder.getCallingUid();
+        if (uid == Process.ROOT_UID || uid == Process.PHONE_UID) return;
+        PermissionUtils.enforceNetworkStackPermission(mContext);
+    }
+
+    private void ensureValidPortNumber(int port) {
+        if (port < 0 || port > 65535) {
+            throw new IllegalArgumentException("Invalid port number " + port);
+        }
+    }
+
+    public ConnectivityNativeService(final Context context) {
+        this(context, new Dependencies());
+    }
+
+    @VisibleForTesting
+    protected ConnectivityNativeService(final Context context, @NonNull Dependencies deps) {
+        mContext = context;
+        mBpfBlockedPortsMap = deps.getBlockPortsMap();
+        attachProgram();
+    }
+
+    @Override
+    public void blockPortForBind(int port) {
+        enforceBlockPortPermission();
+        ensureValidPortNumber(port);
+        try {
+            mBpfBlockedPortsMap.set(port);
+        } catch (ErrnoException e) {
+            throw new ServiceSpecificException(e.errno, e.getMessage());
+        }
+    }
+
+    @Override
+    public void unblockPortForBind(int port) {
+        enforceBlockPortPermission();
+        ensureValidPortNumber(port);
+        try {
+            mBpfBlockedPortsMap.unset(port);
+        } catch (ErrnoException e) {
+            throw new ServiceSpecificException(e.errno,
+                    "Could not unset bitmap value for (port: " + port + "): " + e);
+        }
+    }
+
+    @Override
+    public void unblockAllPortsForBind() {
+        enforceBlockPortPermission();
+        try {
+            mBpfBlockedPortsMap.clear();
+        } catch (ErrnoException e) {
+            throw new ServiceSpecificException(e.errno, "Could not clear map: " + e);
+        }
+    }
+
+    @Override
+    public int[] getPortsBlockedForBind() {
+        enforceBlockPortPermission();
+
+        ArrayList<Integer> portMap = new ArrayList<Integer>();
+        for (int i = 0; i <= 65535; i++) {
+            try {
+                if (mBpfBlockedPortsMap.get(i)) portMap.add(i);
+            } catch (ErrnoException e) {
+                Log.e(TAG, "Failed to get index " + i, e);
+            }
+        }
+        return CollectionUtils.toIntArray(portMap);
+    }
+
+    @Override
+    public int getInterfaceVersion() {
+        return this.VERSION;
+    }
+
+    @Override
+    public String getInterfaceHash() {
+        return this.HASH;
+    }
+
+    /**
+     * Attach BPF program
+     */
+    private void attachProgram() {
+        try {
+            BpfUtils.attachProgram(BPF_CGROUP_INET4_BIND, V4_PROG_PATH, CGROUP_PATH, 0);
+        } catch (IOException e) {
+            throw new UnsupportedOperationException("Unable to attach to BPF_CGROUP_INET4_BIND: "
+                    + e);
+        }
+        try {
+            BpfUtils.attachProgram(BPF_CGROUP_INET6_BIND, V6_PROG_PATH, CGROUP_PATH, 0);
+        } catch (IOException e) {
+            throw new UnsupportedOperationException("Unable to attach to BPF_CGROUP_INET6_BIND: "
+                    + e);
+        }
+        Log.d(TAG, "Attached BPF_CGROUP_INET4_BIND and BPF_CGROUP_INET6_BIND programs");
+    }
+}
diff --git a/service/src/com/android/server/connectivity/DscpPolicyTracker.java b/service/src/com/android/server/connectivity/DscpPolicyTracker.java
index 53b276e..de9dfe3 100644
--- a/service/src/com/android/server/connectivity/DscpPolicyTracker.java
+++ b/service/src/com/android/server/connectivity/DscpPolicyTracker.java
@@ -19,6 +19,7 @@
 import static android.net.NetworkAgent.DSCP_POLICY_STATUS_DELETED;
 import static android.net.NetworkAgent.DSCP_POLICY_STATUS_INSUFFICIENT_PROCESSING_RESOURCES;
 import static android.net.NetworkAgent.DSCP_POLICY_STATUS_POLICY_NOT_FOUND;
+import static android.net.NetworkAgent.DSCP_POLICY_STATUS_REQUEST_DECLINED;
 import static android.net.NetworkAgent.DSCP_POLICY_STATUS_SUCCESS;
 import static android.system.OsConstants.ETH_P_ALL;
 
@@ -37,6 +38,7 @@
 import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.NetworkInterface;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Set;
 
@@ -66,30 +68,68 @@
 
     private final BpfMap<Struct.U32, DscpPolicyValue> mBpfDscpIpv4Policies;
     private final BpfMap<Struct.U32, DscpPolicyValue> mBpfDscpIpv6Policies;
-    private final SparseIntArray mPolicyIdToBpfMapIndex;
+
+    // The actual policy rules used by the BPF code to process packets
+    // are in mBpfDscpIpv4Policies and mBpfDscpIpv4Policies. Both of
+    // these can contain up to MAX_POLICIES rules.
+    //
+    // A given policy always consumes one entry in both the IPv4 and
+    // IPv6 maps even if if's an IPv4-only or IPv6-only policy.
+    //
+    // Each interface index has a SparseIntArray of rules which maps a
+    // policy ID to the index of the corresponding rule in the maps.
+    // mIfaceIndexToPolicyIdBpfMapIndex maps the interface index to
+    // the per-interface SparseIntArray.
+    private final HashMap<Integer, SparseIntArray> mIfaceIndexToPolicyIdBpfMapIndex;
 
     public DscpPolicyTracker() throws ErrnoException {
         mAttachedIfaces = new HashSet<String>();
-
-        mPolicyIdToBpfMapIndex = new SparseIntArray(MAX_POLICIES);
+        mIfaceIndexToPolicyIdBpfMapIndex = new HashMap<Integer, SparseIntArray>();
         mBpfDscpIpv4Policies = new BpfMap<Struct.U32, DscpPolicyValue>(IPV4_POLICY_MAP_PATH,
                 BpfMap.BPF_F_RDWR, Struct.U32.class, DscpPolicyValue.class);
         mBpfDscpIpv6Policies = new BpfMap<Struct.U32, DscpPolicyValue>(IPV6_POLICY_MAP_PATH,
                 BpfMap.BPF_F_RDWR, Struct.U32.class, DscpPolicyValue.class);
     }
 
+    private boolean isUnusedIndex(int index) {
+        for (SparseIntArray ifacePolicies : mIfaceIndexToPolicyIdBpfMapIndex.values()) {
+            if (ifacePolicies.indexOfValue(index) >= 0) return false;
+        }
+        return true;
+    }
+
     private int getFirstFreeIndex() {
+        if (mIfaceIndexToPolicyIdBpfMapIndex.size() == 0) return 0;
         for (int i = 0; i < MAX_POLICIES; i++) {
-            if (mPolicyIdToBpfMapIndex.indexOfValue(i) < 0) return i;
+            if (isUnusedIndex(i)) {
+                return i;
+            }
         }
         return MAX_POLICIES;
     }
 
+    private int findIndex(int policyId, int ifIndex) {
+        SparseIntArray ifacePolicies = mIfaceIndexToPolicyIdBpfMapIndex.get(ifIndex);
+        if (ifacePolicies != null) {
+            final int existingIndex = ifacePolicies.get(policyId, -1);
+            if (existingIndex != -1) {
+                return existingIndex;
+            }
+        }
+
+        final int firstIndex = getFirstFreeIndex();
+        if (firstIndex >= MAX_POLICIES) {
+            // New policy is being added, but max policies has already been reached.
+            return -1;
+        }
+        return firstIndex;
+    }
+
     private void sendStatus(NetworkAgentInfo nai, int policyId, int status) {
         try {
             nai.networkAgent.onDscpPolicyStatusUpdated(policyId, status);
         } catch (RemoteException e) {
-            Log.d(TAG, "Failed update policy status: ", e);
+            Log.e(TAG, "Failed update policy status: ", e);
         }
     }
 
@@ -107,37 +147,43 @@
                         || policy.getSourceAddress() instanceof Inet6Address));
     }
 
-    private int addDscpPolicyInternal(DscpPolicy policy) {
+    private int getIfaceIndex(NetworkAgentInfo nai) {
+        String iface = nai.linkProperties.getInterfaceName();
+        NetworkInterface netIface;
+        try {
+            netIface = NetworkInterface.getByName(iface);
+        } catch (IOException e) {
+            Log.e(TAG, "Unable to get iface index for " + iface + ": " + e);
+            netIface = null;
+        }
+        return (netIface != null) ? netIface.getIndex() : 0;
+    }
+
+    private int addDscpPolicyInternal(DscpPolicy policy, int ifIndex) {
         // If there is no existing policy with a matching ID, and we are already at
         // the maximum number of policies then return INSUFFICIENT_PROCESSING_RESOURCES.
-        final int existingIndex = mPolicyIdToBpfMapIndex.get(policy.getPolicyId(), -1);
-        if (existingIndex == -1 && mPolicyIdToBpfMapIndex.size() >= MAX_POLICIES) {
-            return DSCP_POLICY_STATUS_INSUFFICIENT_PROCESSING_RESOURCES;
+        SparseIntArray ifacePolicies = mIfaceIndexToPolicyIdBpfMapIndex.get(ifIndex);
+        if (ifacePolicies == null) {
+            ifacePolicies = new SparseIntArray(MAX_POLICIES);
         }
 
         // Currently all classifiers are supported, if any are removed return
         // DSCP_POLICY_STATUS_REQUESTED_CLASSIFIER_NOT_SUPPORTED,
         // and for any other generic error DSCP_POLICY_STATUS_REQUEST_DECLINED
 
-        int addIndex = 0;
-        // If a policy with a matching ID exists, replace it, otherwise use the next free
-        // index for the policy.
-        if (existingIndex != -1) {
-            addIndex = mPolicyIdToBpfMapIndex.get(policy.getPolicyId());
-        } else {
-            addIndex = getFirstFreeIndex();
+        final int addIndex = findIndex(policy.getPolicyId(), ifIndex);
+        if (addIndex == -1) {
+            return DSCP_POLICY_STATUS_INSUFFICIENT_PROCESSING_RESOURCES;
         }
 
         try {
-            mPolicyIdToBpfMapIndex.put(policy.getPolicyId(), addIndex);
-
             // Add v4 policy to mBpfDscpIpv4Policies if source and destination address
-            // are both null or if they are both instances of Inet6Address.
+            // are both null or if they are both instances of Inet4Address.
             if (matchesIpv4(policy)) {
                 mBpfDscpIpv4Policies.insertOrReplaceEntry(
                         new Struct.U32(addIndex),
                         new DscpPolicyValue(policy.getSourceAddress(),
-                            policy.getDestinationAddress(),
+                            policy.getDestinationAddress(), ifIndex,
                             policy.getSourcePort(), policy.getDestinationPortRange(),
                             (short) policy.getProtocol(), (short) policy.getDscpValue()));
             }
@@ -148,10 +194,16 @@
                 mBpfDscpIpv6Policies.insertOrReplaceEntry(
                         new Struct.U32(addIndex),
                         new DscpPolicyValue(policy.getSourceAddress(),
-                                policy.getDestinationAddress(),
+                                policy.getDestinationAddress(), ifIndex,
                                 policy.getSourcePort(), policy.getDestinationPortRange(),
                                 (short) policy.getProtocol(), (short) policy.getDscpValue()));
             }
+
+            ifacePolicies.put(policy.getPolicyId(), addIndex);
+            // Only add the policy to the per interface map if the policy was successfully
+            // added to both bpf maps above. It is safe to assume that if insert fails for
+            // one map then it fails for both.
+            mIfaceIndexToPolicyIdBpfMapIndex.put(ifIndex, ifacePolicies);
         } catch (ErrnoException e) {
             Log.e(TAG, "Failed to insert policy into map: ", e);
             return DSCP_POLICY_STATUS_INSUFFICIENT_PROCESSING_RESOURCES;
@@ -166,6 +218,7 @@
      *
      * DSCP_POLICY_STATUS_SUCCESS - if policy was added successfully
      * DSCP_POLICY_STATUS_INSUFFICIENT_PROCESSING_RESOURCES - if max policies were already set
+     * DSCP_POLICY_STATUS_REQUEST_DECLINED - Interface index was invalid
      */
     public void addDscpPolicy(NetworkAgentInfo nai, DscpPolicy policy) {
         if (!mAttachedIfaces.contains(nai.linkProperties.getInterfaceName())) {
@@ -177,11 +230,19 @@
             }
         }
 
-        int status = addDscpPolicyInternal(policy);
+        final int ifIndex = getIfaceIndex(nai);
+        if (ifIndex == 0) {
+            Log.e(TAG, "Iface index is invalid");
+            sendStatus(nai, policy.getPolicyId(), DSCP_POLICY_STATUS_REQUEST_DECLINED);
+            return;
+        }
+
+        int status = addDscpPolicyInternal(policy, ifIndex);
         sendStatus(nai, policy.getPolicyId(), status);
     }
 
-    private void removePolicyFromMap(NetworkAgentInfo nai, int policyId, int index) {
+    private void removePolicyFromMap(NetworkAgentInfo nai, int policyId, int index,
+            boolean sendCallback) {
         int status = DSCP_POLICY_STATUS_POLICY_NOT_FOUND;
         try {
             mBpfDscpIpv4Policies.replaceEntry(new Struct.U32(index), DscpPolicyValue.NONE);
@@ -191,7 +252,9 @@
             Log.e(TAG, "Failed to delete policy from map: ", e);
         }
 
-        sendStatus(nai, policyId, status);
+        if (sendCallback) {
+            sendStatus(nai, policyId, status);
+        }
     }
 
     /**
@@ -204,36 +267,44 @@
             return;
         }
 
-        if (mPolicyIdToBpfMapIndex.get(policyId, -1) != -1) {
-            removePolicyFromMap(nai, policyId, mPolicyIdToBpfMapIndex.get(policyId));
-            mPolicyIdToBpfMapIndex.delete(policyId);
+        SparseIntArray ifacePolicies = mIfaceIndexToPolicyIdBpfMapIndex.get(getIfaceIndex(nai));
+        if (ifacePolicies == null) return;
+
+        final int existingIndex = ifacePolicies.get(policyId, -1);
+        if (existingIndex == -1) {
+            Log.e(TAG, "Policy " + policyId + " does not exist in map.");
+            sendStatus(nai, policyId, DSCP_POLICY_STATUS_POLICY_NOT_FOUND);
+            return;
         }
 
-        // TODO: detach should only occur if no more policies are present on the nai's iface.
-        if (mPolicyIdToBpfMapIndex.size() == 0) {
+        removePolicyFromMap(nai, policyId, existingIndex, true);
+        ifacePolicies.delete(policyId);
+
+        if (ifacePolicies.size() == 0) {
             detachProgram(nai.linkProperties.getInterfaceName());
         }
     }
 
     /**
-     * Remove all DSCP policies and detach program.
+     * Remove all DSCP policies and detach program. Send callback if requested.
      */
-    // TODO: Remove all should only remove policies from corresponding nai iface.
-    public void removeAllDscpPolicies(NetworkAgentInfo nai) {
+    public void removeAllDscpPolicies(NetworkAgentInfo nai, boolean sendCallback) {
         if (!mAttachedIfaces.contains(nai.linkProperties.getInterfaceName())) {
             // Nothing to remove since program is not attached. Send update for policy
             // id 0. The status update must contain a policy ID, and 0 is an invalid id.
-            sendStatus(nai, 0, DSCP_POLICY_STATUS_SUCCESS);
+            if (sendCallback) {
+                sendStatus(nai, 0, DSCP_POLICY_STATUS_SUCCESS);
+            }
             return;
         }
 
-        for (int i = 0; i < mPolicyIdToBpfMapIndex.size(); i++) {
-            removePolicyFromMap(nai, mPolicyIdToBpfMapIndex.keyAt(i),
-                    mPolicyIdToBpfMapIndex.valueAt(i));
+        SparseIntArray ifacePolicies = mIfaceIndexToPolicyIdBpfMapIndex.get(getIfaceIndex(nai));
+        if (ifacePolicies == null) return;
+        for (int i = 0; i < ifacePolicies.size(); i++) {
+            removePolicyFromMap(nai, ifacePolicies.keyAt(i), ifacePolicies.valueAt(i),
+                    sendCallback);
         }
-        mPolicyIdToBpfMapIndex.clear();
-
-        // Can detach program since no policies are active.
+        ifacePolicies.clear();
         detachProgram(nai.linkProperties.getInterfaceName());
     }
 
@@ -241,12 +312,12 @@
      * Attach BPF program
      */
     private boolean attachProgram(@NonNull String iface) {
-        // TODO: attach needs to be per iface not program.
-
         try {
             NetworkInterface netIface = NetworkInterface.getByName(iface);
+            boolean isEth = TcUtils.isEthernet(iface);
+            String path = PROG_PATH + (isEth ? "_ether" : "_raw_ip");
             TcUtils.tcFilterAddDevBpf(netIface.getIndex(), false, PRIO_DSCP, (short) ETH_P_ALL,
-                    PROG_PATH);
+                    path);
         } catch (IOException e) {
             Log.e(TAG, "Unable to attach to TC on " + iface + ": " + e);
             return false;
@@ -264,9 +335,9 @@
             if (netIface != null) {
                 TcUtils.tcFilterDelDev(netIface.getIndex(), false, PRIO_DSCP, (short) ETH_P_ALL);
             }
+            mAttachedIfaces.remove(iface);
         } catch (IOException e) {
             Log.e(TAG, "Unable to detach to TC on " + iface + ": " + e);
         }
-        mAttachedIfaces.remove(iface);
     }
 }
diff --git a/service/src/com/android/server/connectivity/DscpPolicyValue.java b/service/src/com/android/server/connectivity/DscpPolicyValue.java
index cb40306..6e4e7eb 100644
--- a/service/src/com/android/server/connectivity/DscpPolicyValue.java
+++ b/service/src/com/android/server/connectivity/DscpPolicyValue.java
@@ -31,29 +31,31 @@
 public class DscpPolicyValue extends Struct {
     private static final String TAG = DscpPolicyValue.class.getSimpleName();
 
-    // TODO: add the interface index.
     @Field(order = 0, type = Type.ByteArray, arraysize = 16)
     public final byte[] src46;
 
     @Field(order = 1, type = Type.ByteArray, arraysize = 16)
     public final byte[] dst46;
 
-    @Field(order = 2, type = Type.UBE16)
-    public final int srcPort;
+    @Field(order = 2, type = Type.U32)
+    public final long ifIndex;
 
     @Field(order = 3, type = Type.UBE16)
-    public final int dstPortStart;
+    public final int srcPort;
 
     @Field(order = 4, type = Type.UBE16)
+    public final int dstPortStart;
+
+    @Field(order = 5, type = Type.UBE16)
     public final int dstPortEnd;
 
-    @Field(order = 5, type = Type.U8)
+    @Field(order = 6, type = Type.U8)
     public final short proto;
 
-    @Field(order = 6, type = Type.U8)
+    @Field(order = 7, type = Type.U8)
     public final short dscp;
 
-    @Field(order = 7, type = Type.U8, padding = 3)
+    @Field(order = 8, type = Type.U8, padding = 3)
     public final short mask;
 
     private static final int SRC_IP_MASK = 0x1;
@@ -69,6 +71,7 @@
         return true;
     }
 
+    // TODO:  move to frameworks/libs/net and have this and BpfCoordinator import it.
     private byte[] toIpv4MappedAddressBytes(InetAddress ia) {
         final byte[] addr6 = new byte[16];
         if (ia != null) {
@@ -117,13 +120,12 @@
         return mask;
     }
 
-    // This constructor is necessary for BpfMap#getValue since all values must be
-    // in the constructor.
-    public DscpPolicyValue(final InetAddress src46, final InetAddress dst46, final int srcPort,
-            final int dstPortStart, final int dstPortEnd, final short proto,
+    private DscpPolicyValue(final InetAddress src46, final InetAddress dst46, final long ifIndex,
+            final int srcPort, final int dstPortStart, final int dstPortEnd, final short proto,
             final short dscp) {
         this.src46 = toAddressField(src46);
         this.dst46 = toAddressField(dst46);
+        this.ifIndex = ifIndex;
 
         // These params need to be stored as 0 because uints are used in BpfMap.
         // If they are -1 BpfMap write will throw errors.
@@ -138,15 +140,15 @@
         this.mask = makeMask(this.src46, this.dst46, srcPort, dstPortStart, proto, dscp);
     }
 
-    public DscpPolicyValue(final InetAddress src46, final InetAddress dst46, final int srcPort,
-            final Range<Integer> dstPort, final short proto,
+    public DscpPolicyValue(final InetAddress src46, final InetAddress dst46, final long ifIndex,
+            final int srcPort, final Range<Integer> dstPort, final short proto,
             final short dscp) {
-        this(src46, dst46, srcPort, dstPort != null ? dstPort.getLower() : -1,
+        this(src46, dst46, ifIndex, srcPort, dstPort != null ? dstPort.getLower() : -1,
                 dstPort != null ? dstPort.getUpper() : -1, proto, dscp);
     }
 
     public static final DscpPolicyValue NONE = new DscpPolicyValue(
-            null /* src46 */, null /* dst46 */, -1 /* srcPort */,
+            null /* src46 */, null /* dst46 */, 0 /* ifIndex */, -1 /* srcPort */,
             -1 /* dstPortStart */, -1 /* dstPortEnd */, (short) -1 /* proto */,
             (short) 0 /* dscp */);
 
@@ -170,9 +172,9 @@
 
         try {
             return String.format(
-                    "src46: %s, dst46: %s, srcPort: %d, dstPortStart: %d, dstPortEnd: %d,"
-                    + " protocol: %d, dscp %s", srcIpString, dstIpString, srcPort, dstPortStart,
-                    dstPortEnd, proto, dscp);
+                    "src46: %s, dst46: %s, ifIndex: %d, srcPort: %d, dstPortStart: %d,"
+                    + " dstPortEnd: %d, protocol: %d, dscp %s", srcIpString, dstIpString,
+                    ifIndex, srcPort, dstPortStart, dstPortEnd, proto, dscp);
         } catch (IllegalArgumentException e) {
             return String.format("String format error: " + e);
         }
diff --git a/service/src/com/android/server/connectivity/NetworkOffer.java b/service/src/com/android/server/connectivity/NetworkOffer.java
index 1e975dd..eea382e 100644
--- a/service/src/com/android/server/connectivity/NetworkOffer.java
+++ b/service/src/com/android/server/connectivity/NetworkOffer.java
@@ -22,6 +22,7 @@
 import android.net.NetworkRequest;
 import android.os.RemoteException;
 
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.Objects;
 import java.util.Set;
@@ -143,6 +144,11 @@
 
     @Override
     public String toString() {
-        return "NetworkOffer [ Score " + score + " Caps " + caps + "]";
+        final ArrayList<Integer> neededRequestIds = new ArrayList<>();
+        for (final NetworkRequest request : mCurrentlyNeeded) {
+            neededRequestIds.add(request.requestId);
+        }
+        return "NetworkOffer [ Provider Id (" + providerId + ") " + score + " Caps "
+                + caps + " Needed by " + neededRequestIds + "]";
     }
 }
diff --git a/service/src/com/android/server/connectivity/PermissionMonitor.java b/service/src/com/android/server/connectivity/PermissionMonitor.java
index 2885ba7..c02d9cf 100755
--- a/service/src/com/android/server/connectivity/PermissionMonitor.java
+++ b/service/src/com/android/server/connectivity/PermissionMonitor.java
@@ -127,9 +127,19 @@
     @GuardedBy("this")
     private final Set<Integer> mUidsAllowedOnRestrictedNetworks = new ArraySet<>();
 
+    // Store PackageManager for each user.
+    // Keys are users, Values are PackageManagers which get from each user.
     @GuardedBy("this")
     private final Map<UserHandle, PackageManager> mUsersPackageManager = new ArrayMap<>();
 
+    // Store appIds traffic permissions for each user.
+    // Keys are users, Values are SparseArrays where each entry maps an appId to the permissions
+    // that appId has within that user. The permissions are a bitmask of PERMISSION_INTERNET and
+    // PERMISSION_UPDATE_DEVICE_STATS, or 0 (PERMISSION_NONE) if the app has neither of those
+    // permissions. They can never be PERMISSION_UNINSTALLED.
+    @GuardedBy("this")
+    private final Map<UserHandle, SparseIntArray> mUsersTrafficPermissions = new ArrayMap<>();
+
     private static final int SYSTEM_APPID = SYSTEM_UID;
 
     private static final int MAX_PERMISSION_UPDATE_LOGS = 40;
@@ -292,14 +302,24 @@
         sendUidsNetworkPermission(uids, true /* add */);
     }
 
-    private void updateAppIdsTrafficPermission(final SparseIntArray appIds,
-            final SparseIntArray extraAppIds) {
-        for (int i = 0; i < extraAppIds.size(); i++) {
-            final int appId = extraAppIds.keyAt(i);
-            final int permission = extraAppIds.valueAt(i);
-            appIds.put(appId, appIds.get(appId) | permission);
+    /**
+     * Calculates permissions for appIds.
+     * Maps each appId to the union of all traffic permissions that the appId has in all users.
+     *
+     * @return The appIds traffic permissions.
+     */
+    private synchronized SparseIntArray makeAppIdsTrafficPermForAllUsers() {
+        final SparseIntArray appIds = new SparseIntArray();
+        // Check appIds permissions from each user.
+        for (UserHandle user : mUsersTrafficPermissions.keySet()) {
+            final SparseIntArray userAppIds = mUsersTrafficPermissions.get(user);
+            for (int i = 0; i < userAppIds.size(); i++) {
+                final int appId = userAppIds.keyAt(i);
+                final int permission = userAppIds.valueAt(i);
+                appIds.put(appId, appIds.get(appId) | permission);
+            }
         }
-        sendAppIdsTrafficPermission(appIds);
+        return appIds;
     }
 
     private SparseIntArray getSystemTrafficPerm() {
@@ -363,6 +383,10 @@
         // mUidsAllowedOnRestrictedNetworks.
         updateUidsAllowedOnRestrictedNetworks(mDeps.getUidsAllowedOnRestrictedNetworks(mContext));
 
+        // Read system traffic permissions when a user removed and put them to USER_ALL because they
+        // are not specific to any particular user.
+        mUsersTrafficPermissions.put(UserHandle.ALL, getSystemTrafficPerm());
+
         final List<UserHandle> usrs = mUserManager.getUserHandles(true /* excludeDying */);
         // Update netd permissions for all users.
         for (UserHandle user : usrs) {
@@ -487,9 +511,16 @@
         final SparseIntArray uids = makeUidsNetworkPerm(apps);
         updateUidsNetworkPermission(uids);
 
-        // App ids traffic permission
-        final SparseIntArray appIds = makeAppIdsTrafficPerm(apps);
-        updateAppIdsTrafficPermission(appIds, getSystemTrafficPerm());
+        // Add new user appIds permissions.
+        final SparseIntArray addedUserAppIds = makeAppIdsTrafficPerm(apps);
+        mUsersTrafficPermissions.put(user, addedUserAppIds);
+        // Generate appIds from all users and send result to netd.
+        final SparseIntArray appIds = makeAppIdsTrafficPermForAllUsers();
+        sendAppIdsTrafficPermission(appIds);
+
+        // Log user added
+        mPermissionUpdateLogs.log("New user(" + user.getIdentifier() + ") added: nPerm uids="
+                + uids + ", tPerm appIds=" + addedUserAppIds);
     }
 
     /**
@@ -502,6 +533,7 @@
     public synchronized void onUserRemoved(@NonNull UserHandle user) {
         mUsers.remove(user);
 
+        // Remove uids network permissions that belongs to the user.
         final SparseIntArray removedUids = new SparseIntArray();
         final SparseIntArray allUids = mUidToNetworkPerm.clone();
         for (int i = 0; i < allUids.size(); i++) {
@@ -512,6 +544,31 @@
             }
         }
         sendUidsNetworkPermission(removedUids, false /* add */);
+
+        // Remove appIds traffic permission that belongs to the user
+        final SparseIntArray removedUserAppIds = mUsersTrafficPermissions.remove(user);
+        // Generate appIds from the remaining users.
+        final SparseIntArray appIds = makeAppIdsTrafficPermForAllUsers();
+
+        if (removedUserAppIds == null) {
+            Log.wtf(TAG, "onUserRemoved: Receive unknown user=" + user);
+            return;
+        }
+
+        // Clear permission on those appIds belong to this user only, set the permission to
+        // PERMISSION_UNINSTALLED.
+        for (int i = 0; i < removedUserAppIds.size(); i++) {
+            final int appId = removedUserAppIds.keyAt(i);
+            // Need to clear permission if the removed appId is not found in the array.
+            if (appIds.indexOfKey(appId) < 0) {
+                appIds.put(appId, PERMISSION_UNINSTALLED);
+            }
+        }
+        sendAppIdsTrafficPermission(appIds);
+
+        // Log user removed
+        mPermissionUpdateLogs.log("User(" + user.getIdentifier() + ") removed: nPerm uids="
+                + removedUids + ", tPerm appIds=" + removedUserAppIds);
     }
 
     /**
@@ -598,6 +655,39 @@
         }
     }
 
+    private synchronized void updateAppIdTrafficPermission(int uid) {
+        final int uidTrafficPerm = getTrafficPermissionForUid(uid);
+        final SparseIntArray userTrafficPerms =
+                mUsersTrafficPermissions.get(UserHandle.getUserHandleForUid(uid));
+        if (userTrafficPerms == null) {
+            Log.wtf(TAG, "Can't get user traffic permission from uid=" + uid);
+            return;
+        }
+        // Do not put PERMISSION_UNINSTALLED into the array. If no package left on the uid
+        // (PERMISSION_UNINSTALLED), remove the appId from the array. Otherwise, update the latest
+        // permission to the appId.
+        final int appId = UserHandle.getAppId(uid);
+        if (uidTrafficPerm == PERMISSION_UNINSTALLED) {
+            userTrafficPerms.delete(appId);
+        } else {
+            userTrafficPerms.put(appId, uidTrafficPerm);
+        }
+    }
+
+    private synchronized int getAppIdTrafficPermission(int appId) {
+        int permission = PERMISSION_NONE;
+        boolean installed = false;
+        for (UserHandle user : mUsersTrafficPermissions.keySet()) {
+            final SparseIntArray userApps = mUsersTrafficPermissions.get(user);
+            final int appIdx = userApps.indexOfKey(appId);
+            if (appIdx >= 0) {
+                permission |= userApps.valueAt(appIdx);
+                installed = true;
+            }
+        }
+        return installed ? permission : PERMISSION_UNINSTALLED;
+    }
+
     /**
      * Called when a package is added.
      *
@@ -607,9 +697,12 @@
      * @hide
      */
     public synchronized void onPackageAdded(@NonNull final String packageName, final int uid) {
+        // Update uid permission.
+        updateAppIdTrafficPermission(uid);
+        // Get the appId permission from all users then send the latest permission to netd.
         final int appId = UserHandle.getAppId(uid);
-        final int trafficPerm = getTrafficPermissionForUid(uid);
-        sendPackagePermissionsForAppId(appId, trafficPerm);
+        final int appIdTrafficPerm = getAppIdTrafficPermission(appId);
+        sendPackagePermissionsForAppId(appId, appIdTrafficPerm);
 
         final int currentPermission = mUidToNetworkPerm.get(uid, PERMISSION_NONE);
         final int permission = highestPermissionForUid(uid, currentPermission, packageName);
@@ -633,10 +726,12 @@
         // package can bypass VPN.
         updateVpnUid(uid, true /* add */);
         mAllApps.add(appId);
+
+        // Log package added.
         mPermissionUpdateLogs.log("Package add: name=" + packageName + ", uid=" + uid
                 + ", nPerm=(" + permissionToString(permission) + "/"
                 + permissionToString(currentPermission) + ")"
-                + ", tPerm=" + permissionToString(trafficPerm));
+                + ", tPerm=" + permissionToString(appIdTrafficPerm));
     }
 
     private int highestUidNetworkPermission(int uid) {
@@ -664,9 +759,12 @@
      * @hide
      */
     public synchronized void onPackageRemoved(@NonNull final String packageName, final int uid) {
+        // Update uid permission.
+        updateAppIdTrafficPermission(uid);
+        // Get the appId permission from all users then send the latest permission to netd.
         final int appId = UserHandle.getAppId(uid);
-        final int trafficPerm = getTrafficPermissionForUid(uid);
-        sendPackagePermissionsForAppId(appId, trafficPerm);
+        final int appIdTrafficPerm = getAppIdTrafficPermission(appId);
+        sendPackagePermissionsForAppId(appId, appIdTrafficPerm);
 
         // If the newly-removed package falls within some VPN's uid range, update Netd with it.
         // This needs to happen before the mUidToNetworkPerm update below, since
@@ -680,10 +778,13 @@
 
         final int currentPermission = mUidToNetworkPerm.get(uid, PERMISSION_NONE);
         final int permission = highestUidNetworkPermission(uid);
+
+        // Log package removed.
         mPermissionUpdateLogs.log("Package remove: name=" + packageName + ", uid=" + uid
                 + ", nPerm=(" + permissionToString(permission) + "/"
                 + permissionToString(currentPermission) + ")"
-                + ", tPerm=" + permissionToString(trafficPerm));
+                + ", tPerm=" + permissionToString(appIdTrafficPerm));
+
         if (permission != currentPermission) {
             final SparseIntArray apps = new SparseIntArray();
             int sdkSandboxUid = -1;
@@ -889,10 +990,6 @@
      */
     @VisibleForTesting
     void sendAppIdsTrafficPermission(SparseIntArray netdPermissionsAppIds) {
-        if (mNetd == null) {
-            Log.e(TAG, "Failed to get the netd service");
-            return;
-        }
         final ArrayList<Integer> allPermissionAppIds = new ArrayList<>();
         final ArrayList<Integer> internetPermissionAppIds = new ArrayList<>();
         final ArrayList<Integer> updateStatsPermissionAppIds = new ArrayList<>();
diff --git a/service-t/src/com/android/server/net/DelayedDiskWrite.java b/service/src/com/android/server/net/DelayedDiskWrite.java
similarity index 100%
rename from service-t/src/com/android/server/net/DelayedDiskWrite.java
rename to service/src/com/android/server/net/DelayedDiskWrite.java
diff --git a/tests/common/Android.bp b/tests/common/Android.bp
index b23074d..509e881 100644
--- a/tests/common/Android.bp
+++ b/tests/common/Android.bp
@@ -36,6 +36,7 @@
         "modules-utils-build",
         "net-tests-utils",
         "net-utils-framework-common",
+        "platform-compat-test-rules",
         "platform-test-annotations",
     ],
     libs: [
@@ -43,10 +44,23 @@
     ],
 }
 
-// Connectivity coverage tests combines Tethering and Connectivity tests, each with their
-// respective jarjar rules applied.
-// Some tests may be duplicated (in particular static lib tests), as they need to be run under both
-// jarjared packages to cover both usages.
+// Combine Connectivity, NetworkStack and Tethering jarjar rules for coverage target.
+// The jarjar files are simply concatenated in the order specified in srcs.
+// jarjar stops at the first matching rule, so order of concatenation affects the output.
+genrule {
+    name: "ConnectivityCoverageJarJarRules",
+    srcs: [
+        "tethering-jni-jarjar-rules.txt",
+        ":connectivity-jarjar-rules",
+        ":TetheringTestsJarJarRules",
+        ":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"],
+}
+
 android_library {
     name: "ConnectivityCoverageTestsLib",
     min_sdk_version: "30",
@@ -54,8 +68,11 @@
         "FrameworksNetTestsLib",
         "NetdStaticLibTestsLib",
         "NetworkStaticLibTestsLib",
+        "NetworkStackTestsLib",
+        "TetheringTestsLatestSdkLib",
+        "TetheringIntegrationTestsLatestSdkLib",
     ],
-    jarjar_rules: ":connectivity-jarjar-rules",
+    jarjar_rules: ":ConnectivityCoverageJarJarRules",
     manifest: "AndroidManifest_coverage.xml",
     visibility: ["//visibility:private"],
 }
@@ -80,7 +97,6 @@
         "mockito-target-extended-minus-junit4",
         "modules-utils-native-coverage-listener",
         "ConnectivityCoverageTestsLib",
-        "TetheringCoverageTestsLib",
     ],
     jni_libs: [
         // For mockito extended
diff --git a/tests/common/AndroidTest_Coverage.xml b/tests/common/AndroidTest_Coverage.xml
index 7ad0863..48d26b8 100644
--- a/tests/common/AndroidTest_Coverage.xml
+++ b/tests/common/AndroidTest_Coverage.xml
@@ -19,6 +19,8 @@
     </target_preparer>
 
     <option name="test-tag" value="ConnectivityCoverageTests" />
+    <!-- Tethering/Connectivity is a SDK 30+ module -->
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.Sdk30ModuleController" />
     <option name="config-descriptor:metadata" key="mainline-param" value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex" />
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="com.android.connectivity.tests.coverage" />
diff --git a/tests/common/java/android/net/LinkPropertiesTest.java b/tests/common/java/android/net/LinkPropertiesTest.java
index 4d85a57..8fc636a 100644
--- a/tests/common/java/android/net/LinkPropertiesTest.java
+++ b/tests/common/java/android/net/LinkPropertiesTest.java
@@ -20,6 +20,7 @@
 import static android.net.RouteInfo.RTN_UNICAST;
 import static android.net.RouteInfo.RTN_UNREACHABLE;
 
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
 import static com.android.testutils.ParcelUtils.parcelingRoundTrip;
 
@@ -30,6 +31,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import android.compat.testing.PlatformCompatChangeRule;
 import android.net.LinkProperties.ProvisioningChange;
 import android.os.Build;
 import android.system.OsConstants;
@@ -45,6 +47,9 @@
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 
+import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -65,6 +70,9 @@
     @Rule
     public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
 
+    @Rule
+    public final PlatformCompatChangeRule compatChangeRule = new PlatformCompatChangeRule();
+
     private static final InetAddress ADDRV4 = address("75.208.6.1");
     private static final InetAddress ADDRV6 = address("2001:0db8:85a3:0000:0000:8a2e:0370:7334");
     private static final InetAddress DNS1 = address("75.208.7.1");
@@ -1254,6 +1262,7 @@
     }
 
     @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @EnableCompatChanges({LinkProperties.EXCLUDED_ROUTES})
     public void testRouteAddWithSameKey() throws Exception {
         LinkProperties lp = new LinkProperties();
         lp.setInterfaceName("wlan0");
@@ -1268,4 +1277,36 @@
         lp.addRoute(new RouteInfo(v4, address("192.0.2.1"), "wlan0", RTN_THROW, 1460));
         assertEquals(2, lp.getRoutes().size());
     }
+
+    @Test @IgnoreUpTo(SC_V2)
+    @EnableCompatChanges({LinkProperties.EXCLUDED_ROUTES})
+    public void testExcludedRoutesEnabled() {
+        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, 0), RTN_THROW));
+        assertEquals(2, lp.getRoutes().size());
+
+        lp.addRoute(new RouteInfo(GATEWAY1));
+        assertEquals(3, lp.getRoutes().size());
+    }
+
+    @Test @IgnoreUpTo(SC_V2)
+    @DisableCompatChanges({LinkProperties.EXCLUDED_ROUTES})
+    public void testExcludedRoutesDisabled() {
+        final LinkProperties lp = new LinkProperties();
+        assertEquals(0, lp.getRoutes().size());
+
+        lp.addRoute(new RouteInfo(new IpPrefix(ADDRV4, 0), RTN_UNREACHABLE));
+        assertEquals(0, lp.getRoutes().size());
+
+        lp.addRoute(new RouteInfo(new IpPrefix(ADDRV6, 5), RTN_THROW));
+        assertEquals(0, lp.getRoutes().size());
+
+        lp.addRoute(new RouteInfo(new IpPrefix(ADDRV6, 2), RTN_UNICAST));
+        assertEquals(1, lp.getRoutes().size());
+    }
 }
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index 9ae5fab..c30e1d3 100644
--- a/tests/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -82,12 +82,11 @@
 import android.util.ArraySet;
 import android.util.Range;
 
-import androidx.test.runner.AndroidJUnit4;
-
 import com.android.testutils.CompatUtil;
 import com.android.testutils.ConnectivityModuleTest;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Rule;
 import org.junit.Test;
@@ -99,8 +98,12 @@
 import java.util.List;
 import java.util.Set;
 
-@RunWith(AndroidJUnit4.class)
 @SmallTest
+@RunWith(DevSdkIgnoreRunner.class)
+// NetworkCapabilities is only updatable on S+, and this test covers behavior which implementation
+// is self-contained within NetworkCapabilities.java, so it does not need to be run on, or
+// compatible with, earlier releases.
+@IgnoreUpTo(Build.VERSION_CODES.R)
 @ConnectivityModuleTest
 public class NetworkCapabilitiesTest {
     private static final String TEST_SSID = "TEST_SSID";
@@ -489,7 +492,7 @@
         assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
     public void testOemPrivate() {
         NetworkCapabilities nc = new NetworkCapabilities();
         // By default OEM_PRIVATE is neither in the required or forbidden lists and the network is
@@ -516,7 +519,7 @@
         assertFalse(nr.satisfiedByNetworkCapabilities(new NetworkCapabilities()));
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
     public void testForbiddenCapabilities() {
         NetworkCapabilities network = new NetworkCapabilities();
 
@@ -630,7 +633,7 @@
         return new Range<Integer>(from, to);
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testSetAdministratorUids() {
         NetworkCapabilities nc =
                 new NetworkCapabilities().setAdministratorUids(new int[] {2, 1, 3});
@@ -638,7 +641,7 @@
         assertArrayEquals(new int[] {1, 2, 3}, nc.getAdministratorUids());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testSetAdministratorUidsWithDuplicates() {
         try {
             new NetworkCapabilities().setAdministratorUids(new int[] {1, 1});
@@ -750,7 +753,7 @@
                 () -> nc2.addTransportType(TRANSPORT_WIFI));
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R) // New behavior in updatable NetworkCapabilities (S+)
+    @Test
     public void testSetNetworkSpecifierOnTestMultiTransportNc() {
         final NetworkSpecifier specifier = CompatUtil.makeEthernetNetworkSpecifier("eth0");
         NetworkCapabilities nc = new NetworkCapabilities.Builder()
@@ -859,7 +862,7 @@
         assertEquals(TRANSPORT_TEST, transportTypes[3]);
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testTelephonyNetworkSpecifier() {
         final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(1);
         final NetworkCapabilities nc1 = new NetworkCapabilities.Builder()
@@ -970,7 +973,7 @@
         assertEquals(specifier, nc.getNetworkSpecifier());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testAdministratorUidsAndOwnerUid() {
         // Test default owner uid.
         // If the owner uid is not set, the default value should be Process.INVALID_UID.
@@ -1014,7 +1017,7 @@
         return nc;
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
     public void testSubIds() throws Exception {
         final NetworkCapabilities ncWithoutId = capsWithSubIds();
         final NetworkCapabilities ncWithId = capsWithSubIds(TEST_SUBID1);
@@ -1036,7 +1039,7 @@
         assertTrue(requestWithoutId.canBeSatisfiedBy(ncWithId));
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
     public void testEqualsSubIds() throws Exception {
         assertEquals(capsWithSubIds(), capsWithSubIds());
         assertNotEquals(capsWithSubIds(), capsWithSubIds(TEST_SUBID1));
@@ -1185,7 +1188,7 @@
         }
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testBuilder() {
         final int ownerUid = 1001;
         final int signalStrength = -80;
@@ -1255,7 +1258,7 @@
         }
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
     public void testBuilderWithoutDefaultCap() {
         final NetworkCapabilities nc =
                 NetworkCapabilities.Builder.withoutDefaultCapabilities().build();
@@ -1266,12 +1269,12 @@
         assertEquals(0, nc.getCapabilities().length);
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
     public void testRestrictCapabilitiesForTestNetworkByNotOwnerWithNonRestrictedNc() {
         testRestrictCapabilitiesForTestNetworkWithNonRestrictedNc(false /* isOwner */);
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
     public void testRestrictCapabilitiesForTestNetworkByOwnerWithNonRestrictedNc() {
         testRestrictCapabilitiesForTestNetworkWithNonRestrictedNc(true /* isOwner */);
     }
@@ -1316,12 +1319,12 @@
         assertEquals(expectedNcBuilder.build(), nonRestrictedNc);
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
     public void testRestrictCapabilitiesForTestNetworkByNotOwnerWithRestrictedNc() {
         testRestrictCapabilitiesForTestNetworkWithRestrictedNc(false /* isOwner */);
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
     public void testRestrictCapabilitiesForTestNetworkByOwnerWithRestrictedNc() {
         testRestrictCapabilitiesForTestNetworkWithRestrictedNc(true /* isOwner */);
     }
diff --git a/tests/common/tethering-jni-jarjar-rules.txt b/tests/common/tethering-jni-jarjar-rules.txt
new file mode 100644
index 0000000..593ba14
--- /dev/null
+++ b/tests/common/tethering-jni-jarjar-rules.txt
@@ -0,0 +1,10 @@
+# Match the tethering jarjar rules for utils backed by
+# libcom_android_networkstack_tethering_util_jni, so that this JNI library can be used as-is in the
+# test. The alternative would be to build a test-specific JNI library
+# (libcom_android_connectivity_tests_coverage_jni ?) that registers classes following whatever
+# jarjar rules the test is using, but this is a bit less realistic (using a different JNI library),
+# and complicates the test build. It would be necessary if TetheringUtils had a different package
+# name in test code though, as the JNI library name is deducted from the TetheringUtils package.
+rule com.android.net.module.util.BpfMap* com.android.networkstack.tethering.util.BpfMap@1
+rule com.android.net.module.util.BpfUtils* com.android.networkstack.tethering.util.BpfUtils@1
+rule com.android.net.module.util.TcUtils* com.android.networkstack.tethering.util.TcUtils@1
diff --git a/tests/cts/OWNERS b/tests/cts/OWNERS
index d782008..875b4a2 100644
--- a/tests/cts/OWNERS
+++ b/tests/cts/OWNERS
@@ -1,4 +1,3 @@
 # Bug template url: http://b/new?component=31808
-# Bug component: 685852 = per-file **IpSec*
 set noparent
 file:platform/packages/modules/Connectivity:master:/OWNERS_core_networking_xts
diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
index 28437c2..e7b2815 100644
--- a/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
+++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
@@ -28,5 +28,5 @@
     void sendNotification(int notificationId, String notificationType);
     void registerNetworkCallback(in NetworkRequest request, in INetworkCallback cb);
     void unregisterNetworkCallback();
-    void scheduleJob(in JobInfo jobInfo);
+    int scheduleJob(in JobInfo jobInfo);
 }
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
index a840242..96ce65f 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
@@ -16,6 +16,7 @@
 
 package com.android.cts.net.hostside;
 
+import static android.app.job.JobScheduler.RESULT_SUCCESS;
 import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
 import static android.os.BatteryManager.BATTERY_PLUGGED_AC;
 import static android.os.BatteryManager.BATTERY_PLUGGED_USB;
@@ -149,9 +150,9 @@
 
     private static final String APP_NOT_FOREGROUND_ERROR = "app_not_fg";
 
-    protected static final long TEMP_POWERSAVE_WHITELIST_DURATION_MS = 5_000; // 5 sec
+    protected static final long TEMP_POWERSAVE_WHITELIST_DURATION_MS = 20_000; // 20 sec
 
-    private static final long BROADCAST_TIMEOUT_MS = 15_000;
+    private static final long BROADCAST_TIMEOUT_MS = 5_000;
 
     protected Context mContext;
     protected Instrumentation mInstrumentation;
@@ -855,7 +856,8 @@
                     .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
                     .setTransientExtras(extras)
                     .build();
-            mServiceClient.scheduleJob(jobInfo);
+            assertEquals("Error scheduling " + jobInfo,
+                    RESULT_SUCCESS, mServiceClient.scheduleJob(jobInfo));
             forceRunJob(TEST_APP2_PKG, TEST_JOB_ID);
             if (latch.await(JOB_NETWORK_STATE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
                 final int resultCode = result.get(0).first;
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
index 8b70f9b..0610774 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
@@ -107,7 +107,7 @@
         mService.unregisterNetworkCallback();
     }
 
-    public void scheduleJob(JobInfo jobInfo) throws RemoteException {
-        mService.scheduleJob(jobInfo);
+    public int scheduleJob(JobInfo jobInfo) throws RemoteException {
+        return mService.scheduleJob(jobInfo);
     }
 }
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
index 89a9bd6..56be3e3 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
@@ -25,7 +25,7 @@
 import static android.net.wifi.WifiConfiguration.METERED_OVERRIDE_METERED;
 import static android.net.wifi.WifiConfiguration.METERED_OVERRIDE_NONE;
 
-import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
 import static com.android.cts.net.hostside.AbstractRestrictBackgroundNetworkTestCase.TAG;
 
 import static org.junit.Assert.assertEquals;
@@ -38,6 +38,7 @@
 import android.app.Instrumentation;
 import android.app.UiAutomation;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.location.LocationManager;
 import android.net.ConnectivityManager;
 import android.net.ConnectivityManager.NetworkCallback;
@@ -99,6 +100,10 @@
         return mBatterySaverSupported;
     }
 
+    private static boolean isWear() {
+        return getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
+    }
+
     /**
      * As per CDD requirements, if the device doesn't support data saver mode then
      * ConnectivityManager.getRestrictBackgroundStatus() will always return
@@ -107,6 +112,9 @@
      * RESTRICT_BACKGROUND_STATUS_DISABLED or not.
      */
     public static boolean isDataSaverSupported() {
+        if (isWear()) {
+            return false;
+        }
         if (mDataSaverSupported == null) {
             assertMyRestrictBackgroundStatus(RESTRICT_BACKGROUND_STATUS_DISABLED);
             try {
@@ -382,7 +390,7 @@
     }
 
     public static String executeShellCommand(String command) {
-        final String result = runShellCommand(command).trim();
+        final String result = runShellCommandOrThrow(command).trim();
         Log.d(TAG, "Output of '" + command + "': '" + result + "'");
         return result;
     }
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
index f2a7b3f..3ed5391 100644
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
@@ -165,10 +165,10 @@
         }
 
         @Override
-        public void scheduleJob(JobInfo jobInfo) {
+        public int scheduleJob(JobInfo jobInfo) {
             final JobScheduler jobScheduler = getApplicationContext()
                     .getSystemService(JobScheduler.class);
-            jobScheduler.schedule(jobInfo);
+            return jobScheduler.schedule(jobInfo);
         }
       };
 
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index d40bc9f..a129108 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -151,6 +151,7 @@
 import android.os.Looper;
 import android.os.MessageQueue;
 import android.os.Process;
+import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.UserHandle;
@@ -590,6 +591,7 @@
     }
 
     @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+    @AppModeFull(reason = "Cannot get installed packages in instant app mode")
     @Test
     public void testGetRedactedLinkPropertiesForPackage() throws Exception {
         final String groundedPkg = findPackageByPermissions(
@@ -660,10 +662,12 @@
 
             // CaptivePortalApiUrl & CaptivePortalData will be preserved if the given uid holds the
             // NETWORK_SETTINGS permission.
-            assertEquals(capportUrl,
+            assertNotNull(lp.getCaptivePortalApiUrl());
+            assertNotNull(lp.getCaptivePortalData());
+            assertEquals(lp.getCaptivePortalApiUrl(),
                     mCm.getRedactedLinkPropertiesForPackage(lp, privilegedUid, privilegedPkg)
                             .getCaptivePortalApiUrl());
-            assertEquals(capportData,
+            assertEquals(lp.getCaptivePortalData(),
                     mCm.getRedactedLinkPropertiesForPackage(lp, privilegedUid, privilegedPkg)
                             .getCaptivePortalData());
         });
@@ -675,6 +679,7 @@
     }
 
     @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+    @AppModeFull(reason = "Cannot get installed packages in instant app mode")
     @Test
     public void testGetRedactedNetworkCapabilitiesForPackage() throws Exception {
         final String groundedPkg = findPackageByPermissions(
@@ -885,9 +890,21 @@
         //
         // Note that this test this will still fail in instant mode if a device supports Ethernet
         // via other hardware means. We are not currently aware of any such device.
-        return (mContext.getSystemService(Context.ETHERNET_SERVICE) != null) ||
-            mPackageManager.hasSystemFeature(FEATURE_ETHERNET) ||
-            mPackageManager.hasSystemFeature(FEATURE_USB_HOST);
+        return hasEthernetService()
+                || mPackageManager.hasSystemFeature(FEATURE_ETHERNET)
+                || mPackageManager.hasSystemFeature(FEATURE_USB_HOST);
+    }
+
+    private boolean hasEthernetService() {
+        // On Q creating EthernetManager from a thread that does not have a looper (like the test
+        // thread) crashes because it tried to use Looper.myLooper() through the default Handler
+        // constructor to run onAvailabilityChanged callbacks. Use ServiceManager to check whether
+        // the service exists instead.
+        // TODO: remove once Q is no longer supported in MTS, as ServiceManager is hidden API
+        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
+            return ServiceManager.getService(Context.ETHERNET_SERVICE) != null;
+        }
+        return mContext.getSystemService(Context.ETHERNET_SERVICE) != null;
     }
 
     private boolean shouldBeSupported(int networkType) {
diff --git a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
index 9cd8418..1e42fe6 100644
--- a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
+++ b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
@@ -48,9 +48,11 @@
 import android.platform.test.annotations.AppModeFull
 import android.system.Os
 import android.system.OsConstants.AF_INET
+import android.system.OsConstants.AF_INET6
 import android.system.OsConstants.IPPROTO_UDP
 import android.system.OsConstants.SOCK_DGRAM
 import android.system.OsConstants.SOCK_NONBLOCK
+import android.util.Log
 import android.util.Range
 import androidx.test.InstrumentationRegistry
 import androidx.test.runner.AndroidJUnit4
@@ -71,6 +73,8 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import java.net.Inet4Address
+import java.net.Inet6Address
+import java.net.InetAddress
 import java.nio.ByteBuffer
 import java.nio.ByteOrder
 import java.util.regex.Pattern
@@ -81,6 +85,9 @@
 
 private const val MAX_PACKET_LENGTH = 1500
 
+private const val IP4_PREFIX_LEN = 32
+private const val IP6_PREFIX_LEN = 128
+
 private val instrumentation: Instrumentation
     get() = InstrumentationRegistry.getInstrumentation()
 
@@ -97,6 +104,9 @@
     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")
+    private val TEST_TARGET_IPV6_ADDR =
+            InetAddresses.parseNumericAddress("2001:4860:4860::8888") as Inet6Address
 
     private val realContext = InstrumentationRegistry.getContext()
     private val cm = realContext.getSystemService(ConnectivityManager::class.java)
@@ -132,7 +142,9 @@
         runAsShell(MANAGE_TEST_NETWORKS) {
             val tnm = realContext.getSystemService(TestNetworkManager::class.java)
 
-            iface = tnm.createTunInterface(Array(1) { LinkAddress(LOCAL_IPV4_ADDRESS, 32) })
+            iface = tnm.createTunInterface(arrayOf(
+                    LinkAddress(LOCAL_IPV4_ADDRESS, IP4_PREFIX_LEN),
+                    LinkAddress(LOCAL_IPV6_ADDRESS, IP6_PREFIX_LEN)))
             assertNotNull(iface)
         }
 
@@ -146,11 +158,16 @@
 
     @After
     fun tearDown() {
+        if (!kernelIsAtLeast(5, 4)) {
+            return;
+        }
         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)
         handlerThread.quitSafely()
     }
 
@@ -193,9 +210,11 @@
             }
         }
         val lp = LinkProperties().apply {
-            addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
+            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))
-            setInterfaceName(iface.getInterfaceName())
+            addRoute(RouteInfo(InetAddress.getByName("fe80::1234")))
+            setInterfaceName(specifier)
         }
         val config = NetworkAgentConfig.Builder().build()
         val agent = TestableNetworkAgent(context, handlerThread.looper, nc, lp, config)
@@ -215,47 +234,114 @@
         eachByte -> "%02x".format(eachByte)
     }
 
-    fun checkDscpValue(
+    fun sendPacket(
         agent: TestableNetworkAgent,
-        callback: TestableNetworkCallback,
-        dscpValue: Int = 0,
-        dstPort: Int = 0
+        sendV6: Boolean,
+        dstPort: Int = 0,
     ) {
         val testString = "test string"
         val testPacket = ByteBuffer.wrap(testString.toByteArray(Charsets.UTF_8))
         var packetFound = false
 
-        val socket = Os.socket(AF_INET, SOCK_DGRAM or SOCK_NONBLOCK, IPPROTO_UDP)
+        val socket = Os.socket(if (sendV6) AF_INET6 else AF_INET, SOCK_DGRAM or SOCK_NONBLOCK,
+                IPPROTO_UDP)
         agent.network.bindSocket(socket)
 
         val originalPacket = testPacket.readAsArray()
-        Os.sendto(socket, originalPacket, 0 /* bytesOffset */, originalPacket.size,
-                0 /* flags */, TEST_TARGET_IPV4_ADDR, dstPort)
-
+        Os.sendto(socket, originalPacket, 0 /* bytesOffset */, originalPacket.size, 0 /* flags */,
+                if(sendV6) TEST_TARGET_IPV6_ADDR else TEST_TARGET_IPV4_ADDR, dstPort)
         Os.close(socket)
+    }
+
+    fun parseV4PacketDscp(buffer : ByteBuffer) : Int {
+        val ip_ver = buffer.get()
+        val tos = buffer.get()
+        val length = buffer.getShort()
+        val id = buffer.getShort()
+        val offset = buffer.getShort()
+        val ttl = buffer.get()
+        val ipType = buffer.get()
+        val checksum = buffer.getShort()
+        return tos.toInt().shr(2)
+    }
+
+    fun parseV6PacketDscp(buffer : ByteBuffer) : Int {
+        val ip_ver = buffer.get()
+        val tc = buffer.get()
+        val fl = buffer.getShort()
+        val length = buffer.getShort()
+        val proto = buffer.get()
+        val hop = buffer.get()
+        // DSCP is bottom 4 bits of ip_ver and top 2 of tc.
+        val ip_ver_bottom = ip_ver.toInt().and(0xf)
+        val tc_dscp = tc.toInt().shr(6)
+        return ip_ver_bottom.toInt().shl(2) + tc_dscp
+    }
+
+    fun parsePacketIp(
+        buffer : ByteBuffer,
+        sendV6 : Boolean,
+    ) : Boolean {
+        val ipAddr = if (sendV6) ByteArray(16) else ByteArray(4)
+        buffer.get(ipAddr)
+        val srcIp = if (sendV6) Inet6Address.getByAddress(ipAddr)
+                else Inet4Address.getByAddress(ipAddr)
+        buffer.get(ipAddr)
+        val dstIp = if (sendV6) Inet6Address.getByAddress(ipAddr)
+                else Inet4Address.getByAddress(ipAddr)
+
+        Log.e(TAG, "IP Src:" + srcIp + " dst: " + dstIp)
+
+        if ((sendV6 && srcIp == LOCAL_IPV6_ADDRESS && dstIp == TEST_TARGET_IPV6_ADDR) ||
+                (!sendV6 && srcIp == LOCAL_IPV4_ADDRESS && dstIp == TEST_TARGET_IPV4_ADDR)) {
+            Log.e(TAG, "IP return true");
+            return true
+        }
+        Log.e(TAG, "IP return false");
+        return false
+    }
+
+    fun parsePacketPort(
+        buffer : ByteBuffer,
+        srcPort : Int,
+        dstPort : Int
+    ) : Boolean {
+        if (srcPort == 0 && dstPort == 0) return true
+
+        val packetSrcPort = buffer.getShort().toInt()
+        val packetDstPort = buffer.getShort().toInt()
+
+        Log.e(TAG, "Port Src:" + packetSrcPort + " dst: " + packetDstPort)
+
+        if ((srcPort == 0 || (srcPort != 0 && srcPort == packetSrcPort)) &&
+                (dstPort == 0 || (dstPort != 0 && dstPort == packetDstPort))) {
+            Log.e(TAG, "Port return true");
+            return true
+        }
+        Log.e(TAG, "Port return false");
+        return false
+    }
+
+    fun validatePacket(
+        agent : TestableNetworkAgent,
+        sendV6 : Boolean = false,
+        dscpValue : Int = 0,
+        dstPort : Int = 0,
+    ) {
+        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 buffer = ByteBuffer.wrap(packet, 0, packet.size).order(ByteOrder.BIG_ENDIAN)
-            val ip_ver = buffer.get()
-            val tos = buffer.get()
-            val length = buffer.getShort()
-            val id = buffer.getShort()
-            val offset = buffer.getShort()
-            val ttl = buffer.get()
-            val ipType = buffer.get()
-            val checksum = buffer.getShort()
+            val dscp = if (sendV6) parseV6PacketDscp(buffer) else parseV4PacketDscp(buffer)
+            Log.e(TAG, "DSCP value:" + dscp)
 
-            val ipAddr = ByteArray(4)
-            buffer.get(ipAddr)
-            val srcIp = Inet4Address.getByAddress(ipAddr)
-            buffer.get(ipAddr)
-            val dstIp = Inet4Address.getByAddress(ipAddr)
-            val packetSrcPort = buffer.getShort().toInt()
-            val packetDstPort = buffer.getShort().toInt()
-
-            // TODO: Add source port comparison.
-            if (srcIp == LOCAL_IPV4_ADDRESS && dstIp == TEST_TARGET_IPV4_ADDR &&
-                    packetDstPort == dstPort) {
-                assertEquals(dscpValue, (tos.toInt().shr(2)))
+            // TODO: Add source port comparison. Use 0 for now.
+            if (parsePacketIp(buffer, sendV6) && parsePacketPort(buffer, 0, dstPort)) {
+                Log.e(TAG, "DSCP value found")
+                assertEquals(dscpValue, dscp)
                 packetFound = true
             }
         }
@@ -272,12 +358,12 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(policyId, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
-            checkDscpValue(agent, callback, dstPort = portNumber)
         }
     }
 
     @Test
-    fun testDscpPolicyAddPolicies(): Unit = createConnectedNetworkAgent().let { (agent, callback) ->
+    fun testDscpPolicyAddPolicies(): Unit = createConnectedNetworkAgent().let {
+                (agent, callback) ->
         val policy = DscpPolicy.Builder(1, 1)
                 .setDestinationPortRange(Range(4444, 4444)).build()
         agent.sendAddDscpPolicy(policy)
@@ -285,8 +371,7 @@
             assertEquals(1, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
         }
-
-        checkDscpValue(agent, callback, dscpValue = 1, dstPort = 4444)
+        validatePacket(agent, dscpValue = 1, dstPort = 4444)
 
         agent.sendRemoveDscpPolicy(1)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
@@ -295,15 +380,54 @@
         }
 
         val policy2 = DscpPolicy.Builder(1, 4)
-                .setDestinationPortRange(Range(5555, 5555)).setSourceAddress(LOCAL_IPV4_ADDRESS)
-                .setDestinationAddress(TEST_TARGET_IPV4_ADDR).setProtocol(IPPROTO_UDP).build()
+                .setDestinationPortRange(Range(5555, 5555))
+                .setDestinationAddress(TEST_TARGET_IPV4_ADDR)
+                .setSourceAddress(LOCAL_IPV4_ADDRESS)
+                .setProtocol(IPPROTO_UDP).build()
         agent.sendAddDscpPolicy(policy2)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
         }
 
-        checkDscpValue(agent, callback, dscpValue = 4, dstPort = 5555)
+        validatePacket(agent, dscpValue = 4, dstPort = 5555)
+
+        agent.sendRemoveDscpPolicy(1)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(1, it.policyId)
+            assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
+        }
+    }
+
+    @Test
+    fun testDscpPolicyAddV6Policies(): Unit = createConnectedNetworkAgent().let {
+                (agent, callback) ->
+        val policy = DscpPolicy.Builder(1, 1)
+                .setDestinationPortRange(Range(4444, 4444)).build()
+        agent.sendAddDscpPolicy(policy)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(1, it.policyId)
+            assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
+        }
+        validatePacket(agent, true, dscpValue = 1, dstPort = 4444)
+
+        agent.sendRemoveDscpPolicy(1)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(1, it.policyId)
+            assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
+        }
+
+        val policy2 = DscpPolicy.Builder(1, 4)
+                .setDestinationPortRange(Range(5555, 5555))
+                .setDestinationAddress(TEST_TARGET_IPV6_ADDR)
+                .setSourceAddress(LOCAL_IPV6_ADDRESS)
+                .setProtocol(IPPROTO_UDP).build()
+        agent.sendAddDscpPolicy(policy2)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(1, it.policyId)
+            assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
+        }
+        validatePacket(agent, true, dscpValue = 4, dstPort = 5555)
 
         agent.sendRemoveDscpPolicy(1)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
@@ -321,7 +445,7 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 1111)
+            validatePacket(agent, dscpValue = 1, dstPort = 1111)
         }
 
         val policy2 = DscpPolicy.Builder(2, 1).setDestinationPortRange(Range(2222, 2222)).build()
@@ -329,7 +453,7 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(2, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 2222)
+            validatePacket(agent, dscpValue = 1, dstPort = 2222)
         }
 
         val policy3 = DscpPolicy.Builder(3, 1).setDestinationPortRange(Range(3333, 3333)).build()
@@ -337,13 +461,16 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(3, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 3333)
+            validatePacket(agent, dscpValue = 1, dstPort = 3333)
         }
 
         /* Remove Policies and check CE is no longer set */
         doRemovePolicyTest(agent, callback, 1)
+        validatePacket(agent, dscpValue = 0, dstPort = 1111)
         doRemovePolicyTest(agent, callback, 2)
+        validatePacket(agent, dscpValue = 0, dstPort = 2222)
         doRemovePolicyTest(agent, callback, 3)
+        validatePacket(agent, dscpValue = 0, dstPort = 3333)
     }
 
     @Test
@@ -354,7 +481,7 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 1111)
+            validatePacket(agent, dscpValue = 1, dstPort = 1111)
         }
         doRemovePolicyTest(agent, callback, 1)
 
@@ -363,7 +490,7 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(2, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 2222)
+            validatePacket(agent, dscpValue = 1, dstPort = 2222)
         }
         doRemovePolicyTest(agent, callback, 2)
 
@@ -372,7 +499,7 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(3, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 3333)
+            validatePacket(agent, dscpValue = 1, dstPort = 3333)
         }
         doRemovePolicyTest(agent, callback, 3)
     }
@@ -386,7 +513,7 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 1111)
+            validatePacket(agent, dscpValue = 1, dstPort = 1111)
         }
 
         val policy2 = DscpPolicy.Builder(2, 1).setDestinationPortRange(Range(2222, 2222)).build()
@@ -394,7 +521,7 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(2, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 2222)
+            validatePacket(agent, dscpValue = 1, dstPort = 2222)
         }
 
         val policy3 = DscpPolicy.Builder(3, 1).setDestinationPortRange(Range(3333, 3333)).build()
@@ -402,7 +529,7 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(3, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 3333)
+            validatePacket(agent, dscpValue = 1, dstPort = 3333)
         }
 
         /* Remove Policies and check CE is no longer set */
@@ -420,14 +547,15 @@
     }
 
     @Test
-    fun testRemoveAllDscpPolicies(): Unit = createConnectedNetworkAgent().let { (agent, callback) ->
+    fun testRemoveAllDscpPolicies(): Unit = createConnectedNetworkAgent().let {
+                (agent, callback) ->
         val policy = DscpPolicy.Builder(1, 1)
                 .setDestinationPortRange(Range(1111, 1111)).build()
         agent.sendAddDscpPolicy(policy)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 1111)
+            validatePacket(agent, dscpValue = 1, dstPort = 1111)
         }
 
         val policy2 = DscpPolicy.Builder(2, 1)
@@ -436,7 +564,7 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(2, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 2222)
+            validatePacket(agent, dscpValue = 1, dstPort = 2222)
         }
 
         val policy3 = DscpPolicy.Builder(3, 1)
@@ -445,24 +573,24 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(3, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 3333)
+            validatePacket(agent, dscpValue = 1, dstPort = 3333)
         }
 
         agent.sendRemoveAllDscpPolicies()
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
-            checkDscpValue(agent, callback, dstPort = 1111)
+            validatePacket(agent, false, dstPort = 1111)
         }
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(2, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
-            checkDscpValue(agent, callback, dstPort = 2222)
+            validatePacket(agent, false, dstPort = 2222)
         }
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(3, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
-            checkDscpValue(agent, callback, dstPort = 3333)
+            validatePacket(agent, false, dstPort = 3333)
         }
     }
 
@@ -474,12 +602,9 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 4444)
+            validatePacket(agent, dscpValue = 1, dstPort = 4444)
         }
 
-        // TODO: send packet on socket and confirm that changing the DSCP policy
-        // updates the mark to the new value.
-
         val policy2 = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(5555, 5555)).build()
         agent.sendAddDscpPolicy(policy2)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
@@ -487,8 +612,8 @@
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
 
             // Sending packet with old policy should fail
-            checkDscpValue(agent, callback, dstPort = 4444)
-            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 5555)
+            validatePacket(agent, dscpValue = 0, dstPort = 4444)
+            validatePacket(agent, dscpValue = 1, dstPort = 5555)
         }
 
         agent.sendRemoveDscpPolicy(1)
diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
index 2737258..30e0015 100644
--- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
@@ -17,7 +17,9 @@
 
 import android.Manifest.permission.MANAGE_TEST_NETWORKS
 import android.Manifest.permission.NETWORK_SETTINGS
+import android.net.InetAddresses
 import android.net.IpConfiguration
+import android.net.MacAddress
 import android.net.TestNetworkInterface
 import android.net.TestNetworkManager
 import android.platform.test.annotations.AppModeFull
@@ -32,6 +34,7 @@
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
+import android.content.Context
 import org.junit.runner.RunWith
 import kotlin.test.assertNull
 import kotlin.test.fail
@@ -46,10 +49,15 @@
 import com.android.networkstack.apishim.common.EthernetManagerShim.ROLE_CLIENT
 import com.android.networkstack.apishim.common.EthernetManagerShim.ROLE_NONE
 import com.android.networkstack.apishim.EthernetManagerShimImpl
+import com.android.testutils.RouterAdvertisementResponder
+import com.android.testutils.TapPacketReader
+import com.android.testutils.waitForIdle
+import java.net.Inet6Address
 import java.util.concurrent.Executor
 import kotlin.test.assertFalse
 import kotlin.test.assertEquals
 import kotlin.test.assertTrue
+import java.net.NetworkInterface
 
 private const val TIMEOUT_MS = 1000L
 private const val NO_CALLBACK_TIMEOUT_MS = 200L
@@ -66,9 +74,40 @@
     private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
     private val em by lazy { EthernetManagerShimImpl.newInstance(context) }
 
-    private val createdIfaces = ArrayList<TestNetworkInterface>()
+    private val createdIfaces = ArrayList<EthernetTestInterface>()
     private val addedListeners = ArrayList<InterfaceStateListener>()
 
+    private class EthernetTestInterface(
+        context: Context,
+        private val handler: Handler
+    ) {
+        private val tapInterface: TestNetworkInterface
+        private val packetReader: TapPacketReader
+        private val raResponder: RouterAdvertisementResponder
+        val interfaceName get() = tapInterface.interfaceName
+
+        init {
+            tapInterface = runAsShell(MANAGE_TEST_NETWORKS) {
+                val tnm = context.getSystemService(TestNetworkManager::class.java)
+                tnm.createTapInterface(false /* bringUp */)
+            }
+            val mtu = NetworkInterface.getByName(tapInterface.interfaceName).getMTU()
+            packetReader = TapPacketReader(handler, tapInterface.fileDescriptor.fileDescriptor, mtu)
+            raResponder = RouterAdvertisementResponder(packetReader)
+            raResponder.addRouterEntry(MacAddress.fromString("01:23:45:67:89:ab"),
+                    InetAddresses.parseNumericAddress("fe80::abcd") as Inet6Address)
+
+            packetReader.startAsyncForTest()
+            raResponder.start()
+        }
+
+        fun destroy() {
+            raResponder.stop()
+            handler.post({ packetReader.stop() })
+            handler.waitForIdle(TIMEOUT_MS)
+        }
+    }
+
     private open class EthernetStateListener private constructor(
         private val history: ArrayTrackRecord<CallbackEntry>
     ) : InterfaceStateListener,
@@ -101,7 +140,7 @@
             return event as T
         }
 
-        fun expectCallback(iface: TestNetworkInterface, state: Int, role: Int) {
+        fun expectCallback(iface: EthernetTestInterface, state: Int, role: Int) {
             expectCallback(InterfaceStateChanged(iface.interfaceName, state, role,
                 if (state != STATE_ABSENT) DEFAULT_IP_CONFIGURATION else null))
         }
@@ -116,23 +155,55 @@
         }
     }
 
+    @Before
+    fun setUp() {
+        setIncludeTestInterfaces(true)
+    }
+
+    @After
+    fun tearDown() {
+        setIncludeTestInterfaces(false)
+        for (iface in createdIfaces) {
+            iface.destroy()
+        }
+        for (listener in addedListeners) {
+            em.removeInterfaceStateListener(listener)
+        }
+    }
+
+    private fun addInterfaceStateListener(executor: Executor, listener: InterfaceStateListener) {
+        em.addInterfaceStateListener(executor, listener)
+        addedListeners.add(listener)
+    }
+
+    private fun createInterface(): EthernetTestInterface {
+        return EthernetTestInterface(context, Handler(Looper.getMainLooper()))
+    }
+
+    private fun setIncludeTestInterfaces(value: Boolean) {
+        runAsShell(NETWORK_SETTINGS) {
+            em.setIncludeTestInterfaces(value)
+        }
+    }
+
+    private fun removeInterface(iface: EthernetTestInterface) {
+        iface.destroy()
+        createdIfaces.remove(iface)
+    }
+
     @Test
     public fun testCallbacks() {
         val executor = HandlerExecutor(Handler(Looper.getMainLooper()))
 
         // If an interface exists when the callback is registered, it is reported on registration.
-        val iface = runAsShell(MANAGE_TEST_NETWORKS) {
-            createInterface()
-        }
+        val iface = createInterface()
         val listener = EthernetStateListener()
         addInterfaceStateListener(executor, listener)
         listener.expectCallback(iface, STATE_LINK_UP, ROLE_CLIENT)
 
         // If an interface appears, existing callbacks see it.
         // TODO: fix the up/up/down/up callbacks and only send down/up.
-        val iface2 = runAsShell(MANAGE_TEST_NETWORKS) {
-            createInterface()
-        }
+        val iface2 = createInterface()
         listener.expectCallback(iface2, STATE_LINK_UP, ROLE_CLIENT)
         listener.expectCallback(iface2, STATE_LINK_UP, ROLE_CLIENT)
         listener.expectCallback(iface2, STATE_LINK_DOWN, ROLE_CLIENT)
@@ -149,66 +220,25 @@
         listener.assertNoCallback()
     }
 
-    @Before
-    fun setUp() {
-        runAsShell(MANAGE_TEST_NETWORKS, NETWORK_SETTINGS) {
-            em.setIncludeTestInterfaces(true)
-        }
-    }
-
-    @After
-    fun tearDown() {
-        runAsShell(MANAGE_TEST_NETWORKS, NETWORK_SETTINGS) {
-            em.setIncludeTestInterfaces(false)
-            for (iface in createdIfaces) {
-                if (iface.fileDescriptor.fileDescriptor.valid()) iface.fileDescriptor.close()
-            }
-            for (listener in addedListeners) {
-                em.removeInterfaceStateListener(listener)
-            }
-        }
-    }
-
-    private fun addInterfaceStateListener(executor: Executor, listener: InterfaceStateListener) {
-        em.addInterfaceStateListener(executor, listener)
-        addedListeners.add(listener)
-    }
-
-    private fun createInterface(): TestNetworkInterface {
-        val tnm = context.getSystemService(TestNetworkManager::class.java)
-        return tnm.createTapInterface(false /* bringUp */).also { createdIfaces.add(it) }
-    }
-
-    private fun removeInterface(iface: TestNetworkInterface) {
-        iface.fileDescriptor.close()
-        createdIfaces.remove(iface)
-    }
-
-    private fun doTestGetInterfaceList() {
-        em.setIncludeTestInterfaces(true)
+    @Test
+    public fun testGetInterfaceList() {
+        setIncludeTestInterfaces(true)
 
         // Create two test interfaces and check the return list contains the interface names.
         val iface1 = createInterface()
         val iface2 = createInterface()
         var ifaces = em.getInterfaceList()
         assertTrue(ifaces.size > 0)
-        assertTrue(ifaces.contains(iface1.getInterfaceName()))
-        assertTrue(ifaces.contains(iface2.getInterfaceName()))
+        assertTrue(ifaces.contains(iface1.interfaceName))
+        assertTrue(ifaces.contains(iface2.interfaceName))
 
         // Remove one existing test interface and check the return list doesn't contain the
         // removed interface name.
         removeInterface(iface1)
         ifaces = em.getInterfaceList()
-        assertFalse(ifaces.contains(iface1.getInterfaceName()))
-        assertTrue(ifaces.contains(iface2.getInterfaceName()))
+        assertFalse(ifaces.contains(iface1.interfaceName))
+        assertTrue(ifaces.contains(iface2.interfaceName))
 
         removeInterface(iface2)
     }
-
-    @Test
-    public fun testGetInterfaceList() {
-        runAsShell(MANAGE_TEST_NETWORKS, NETWORK_SETTINGS) {
-            doTestGetInterfaceList()
-        }
-    }
 }
diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
index 9fa146f..04843f9 100644
--- a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
+++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
@@ -20,14 +20,19 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
+import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS;
+import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS;
 
 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 import static com.android.testutils.TestableNetworkCallbackKt.anyNetwork;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeTrue;
@@ -46,17 +51,25 @@
 import android.net.TestNetworkInterface;
 import android.net.VpnManager;
 import android.net.cts.util.CtsNetUtils;
+import android.net.ipsec.ike.IkeTunnelConnectionParams;
 import android.os.Build;
 import android.os.Process;
 import android.platform.test.annotations.AppModeFull;
+import android.text.TextUtils;
 
 import androidx.test.InstrumentationRegistry;
 
 import com.android.internal.util.HexDump;
+import com.android.networkstack.apishim.ConstantsShim;
 import com.android.networkstack.apishim.Ikev2VpnProfileBuilderShimImpl;
 import com.android.networkstack.apishim.Ikev2VpnProfileShimImpl;
+import com.android.networkstack.apishim.VpnManagerShimImpl;
+import com.android.networkstack.apishim.common.Ikev2VpnProfileBuilderShim;
 import com.android.networkstack.apishim.common.Ikev2VpnProfileShim;
 import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
+import com.android.networkstack.apishim.common.VpnManagerShim;
+import com.android.networkstack.apishim.common.VpnProfileStateShim;
+import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.RecorderCallback.CallbackEntry;
@@ -64,6 +77,7 @@
 
 import org.bouncycastle.x509.X509V1CertificateGenerator;
 import org.junit.After;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -73,6 +87,7 @@
 import java.security.KeyPairGenerator;
 import java.security.PrivateKey;
 import java.security.cert.X509Certificate;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Date;
 import java.util.List;
@@ -86,6 +101,9 @@
 public class Ikev2VpnTest {
     private static final String TAG = Ikev2VpnTest.class.getSimpleName();
 
+    @Rule
+    public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+
     // Test vectors for IKE negotiation in test mode.
     private static final String SUCCESSFUL_IKE_INIT_RESP_V4 =
             "46b8eca1e0d72a18b2b5d9006d47a0022120222000000000000002d0220000300000002c01010004030000"
@@ -175,8 +193,11 @@
     private static final CtsNetUtils mCtsNetUtils = new CtsNetUtils(sContext);
     private static final long TIMEOUT_MS = 15_000;
 
+    private VpnManagerShim mVmShim = VpnManagerShimImpl.newInstance(sContext);
+
     private final X509Certificate mServerRootCa;
     private final CertificateAndKey mUserCertKey;
+    private final List<TestableNetworkCallback> mCallbacksToUnregister = new ArrayList<>();
 
     public Ikev2VpnTest() throws Exception {
         // Build certificates
@@ -186,6 +207,9 @@
 
     @After
     public void tearDown() {
+        for (TestableNetworkCallback callback : mCallbacksToUnregister) {
+            sCM.unregisterNetworkCallback(callback);
+        }
         setAppop(AppOpsManager.OP_ACTIVATE_VPN, false);
         setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, false);
     }
@@ -204,51 +228,55 @@
         }, Manifest.permission.MANAGE_TEST_NETWORKS);
     }
 
-    private Ikev2VpnProfile buildIkev2VpnProfileCommon(@NonNull Ikev2VpnProfile.Builder builder,
-            boolean isRestrictedToTestNetworks,
+    private Ikev2VpnProfile buildIkev2VpnProfileCommon(
+            @NonNull Ikev2VpnProfileBuilderShim builderShim, boolean isRestrictedToTestNetworks,
             boolean requiresValidation) throws Exception {
-        if (isRestrictedToTestNetworks) {
-            builder.restrictToTestNetworks();
-        }
 
-        builder.setBypassable(true)
+        builderShim.setBypassable(true)
                 .setAllowedAlgorithms(TEST_ALLOWED_ALGORITHMS)
                 .setProxy(TEST_PROXY_INFO)
                 .setMaxMtu(TEST_MTU)
                 .setMetered(false);
         if (TestUtils.shouldTestTApis()) {
-            Ikev2VpnProfileBuilderShimImpl.newInstance().setRequiresInternetValidation(
-                    builder, requiresValidation);
+            builderShim.setRequiresInternetValidation(requiresValidation);
         }
+
+        // Convert shim back to Ikev2VpnProfile.Builder since restrictToTestNetworks is a hidden
+        // method and does not defined in shims.
+        // TODO: replace it in alternative way to remove the hidden method usage
+        final Ikev2VpnProfile.Builder builder = (Ikev2VpnProfile.Builder) builderShim.getBuilder();
+        if (isRestrictedToTestNetworks) {
+            builder.restrictToTestNetworks();
+        }
+
         return builder.build();
     }
 
     private Ikev2VpnProfile buildIkev2VpnProfilePsk(@NonNull String remote,
             boolean isRestrictedToTestNetworks, boolean requiresValidation) throws Exception {
-        final Ikev2VpnProfile.Builder builder =
-                new Ikev2VpnProfile.Builder(remote, TEST_IDENTITY).setAuthPsk(TEST_PSK);
-
+        final Ikev2VpnProfileBuilderShim builder =
+                Ikev2VpnProfileBuilderShimImpl.newInstance(remote, TEST_IDENTITY, null)
+                        .setAuthPsk(TEST_PSK);
         return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
                 requiresValidation);
     }
 
     private Ikev2VpnProfile buildIkev2VpnProfileUsernamePassword(boolean isRestrictedToTestNetworks)
             throws Exception {
-        final Ikev2VpnProfile.Builder builder =
-                new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
-                        .setAuthUsernamePassword(TEST_USER, TEST_PASSWORD, mServerRootCa);
 
+        final Ikev2VpnProfileBuilderShim builder =
+                Ikev2VpnProfileBuilderShimImpl.newInstance(TEST_SERVER_ADDR_V6, TEST_IDENTITY, null)
+                        .setAuthUsernamePassword(TEST_USER, TEST_PASSWORD, mServerRootCa);
         return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
                 false /* requiresValidation */);
     }
 
     private Ikev2VpnProfile buildIkev2VpnProfileDigitalSignature(boolean isRestrictedToTestNetworks)
             throws Exception {
-        final Ikev2VpnProfile.Builder builder =
-                new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
+        final Ikev2VpnProfileBuilderShim builder =
+                Ikev2VpnProfileBuilderShimImpl.newInstance(TEST_SERVER_ADDR_V6, TEST_IDENTITY, null)
                         .setAuthDigitalSignature(
                                 mUserCertKey.cert, mUserCertKey.key, mServerRootCa);
-
         return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
                 false /* requiresValidation */);
     }
@@ -279,18 +307,42 @@
         assertNull(profile.getServerRootCaCert());
         assertNull(profile.getRsaPrivateKey());
         assertNull(profile.getUserCert());
-        final Ikev2VpnProfileShim<Ikev2VpnProfile> shim = Ikev2VpnProfileShimImpl.newInstance();
+        final Ikev2VpnProfileShim<Ikev2VpnProfile> shim = new Ikev2VpnProfileShimImpl(profile);
         if (TestUtils.shouldTestTApis()) {
-            assertEquals(requiresValidation, shim.isInternetValidationRequired(profile));
+            assertEquals(requiresValidation, shim.isInternetValidationRequired());
         } else {
             try {
-                shim.isInternetValidationRequired(profile);
+                shim.isInternetValidationRequired();
                 fail("Only supported from API level 33");
             } catch (UnsupportedApiLevelException expected) {
             }
         }
     }
 
+    @IgnoreUpTo(SC_V2)
+    @Test
+    public void testBuildIkev2VpnProfileWithIkeTunnelConnectionParams() throws Exception {
+        assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+        assumeTrue(TestUtils.shouldTestTApis());
+
+        final IkeTunnelConnectionParams expectedParams =
+                new IkeTunnelConnectionParams(IKE_PARAMS, CHILD_PARAMS);
+        final Ikev2VpnProfileBuilderShim ikeProfileBuilder =
+                Ikev2VpnProfileBuilderShimImpl.newInstance(null, null, expectedParams);
+        // Verify the other Ike options could not be set with IkeTunnelConnectionParams.
+        final Class<IllegalArgumentException> expected = IllegalArgumentException.class;
+        assertThrows(expected, () -> ikeProfileBuilder.setAuthPsk(TEST_PSK));
+        assertThrows(expected, () ->
+                ikeProfileBuilder.setAuthUsernamePassword(TEST_USER, TEST_PASSWORD, mServerRootCa));
+        assertThrows(expected, () -> ikeProfileBuilder.setAuthDigitalSignature(
+                mUserCertKey.cert, mUserCertKey.key, mServerRootCa));
+
+        final Ikev2VpnProfile profile = (Ikev2VpnProfile) ikeProfileBuilder.build().getProfile();
+
+        assertEquals(expectedParams,
+                new Ikev2VpnProfileShimImpl(profile).getIkeTunnelConnectionParams());
+    }
+
     @Test
     public void testBuildIkev2VpnProfilePsk() throws Exception {
         doTestBuildIkev2VpnProfilePsk(true /* requiresValidation */);
@@ -420,7 +472,7 @@
     }
 
     private void checkStartStopVpnProfileBuildsNetworks(@NonNull IkeTunUtils tunUtils,
-            boolean testIpv6, boolean requiresValidation)
+            boolean testIpv6, boolean requiresValidation, boolean testSessionKey)
             throws Exception {
         String serverAddr = testIpv6 ? TEST_SERVER_ADDR_V6 : TEST_SERVER_ADDR_V4;
         String initResp = testIpv6 ? SUCCESSFUL_IKE_INIT_RESP_V6 : SUCCESSFUL_IKE_INIT_RESP_V4;
@@ -437,9 +489,22 @@
         final TestableNetworkCallback cb = new TestableNetworkCallback(TIMEOUT_MS);
         final NetworkRequest nr = new NetworkRequest.Builder()
                 .clearCapabilities().addTransportType(TRANSPORT_VPN).build();
-        sCM.registerNetworkCallback(nr, cb);
+        registerNetworkCallback(nr, cb);
 
-        sVpnMgr.startProvisionedVpnProfile();
+        if (testSessionKey) {
+            // testSessionKey will never be true if running on <T
+            // startProvisionedVpnProfileSession() should return a non-null & non-empty random UUID.
+            final String sessionId = mVmShim.startProvisionedVpnProfileSession();
+            assertFalse(TextUtils.isEmpty(sessionId));
+            final VpnProfileStateShim profileState = mVmShim.getProvisionedVpnProfileState();
+            assertNotNull(profileState);
+            assertEquals(ConstantsShim.VPN_PROFILE_STATE_CONNECTING, profileState.getState());
+            assertEquals(sessionId, profileState.getSessionId());
+            assertFalse(profileState.isAlwaysOn());
+            assertFalse(profileState.isLockdownEnabled());
+        } else {
+            sVpnMgr.startProvisionedVpnProfile();
+        }
 
         // Inject IKE negotiation
         int expectedMsgId = 0;
@@ -452,7 +517,16 @@
         final Network vpnNetwork = cb.expectCallback(CallbackEntry.AVAILABLE, anyNetwork())
                 .getNetwork();
 
-        cb.expectCapabilitiesThat(vpnNetwork, TIMEOUT_MS, caps -> caps.hasTransport(TRANSPORT_VPN)
+        if (testSessionKey) {
+            final VpnProfileStateShim profileState = mVmShim.getProvisionedVpnProfileState();
+            assertNotNull(profileState);
+            assertEquals(ConstantsShim.VPN_PROFILE_STATE_CONNECTED, profileState.getState());
+            assertFalse(profileState.isAlwaysOn());
+            assertFalse(profileState.isLockdownEnabled());
+        }
+
+        cb.expectCapabilitiesThat(vpnNetwork, TIMEOUT_MS,
+                caps -> caps.hasTransport(TRANSPORT_VPN)
                 && caps.hasCapability(NET_CAPABILITY_INTERNET)
                 && !caps.hasCapability(NET_CAPABILITY_VALIDATED)
                 && Process.myUid() == caps.getOwnerUid());
@@ -465,8 +539,10 @@
         // but unexpectedly sends this callback, expecting LOST below will fail because the next
         // callback will be the validated capabilities instead.
         // In S and below, |requiresValidation| is ignored, so this callback is always sent
-        // regardless of its value.
-        if (!requiresValidation || !TestUtils.shouldTestTApis()) {
+        // regardless of its value. However, there is a race in Vpn(see b/228574221) that VPN may
+        // misuse VPN network itself as the underlying network. The fix is not available without
+        // SDK > T platform. Thus, verify this only on T+ platform.
+        if (!requiresValidation && TestUtils.shouldTestTApis()) {
             cb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED, TIMEOUT_MS,
                     entry -> ((CallbackEntry.CapabilitiesChanged) entry).getCaps()
                             .hasCapability(NET_CAPABILITY_VALIDATED));
@@ -479,19 +555,28 @@
                 lost -> vpnNetwork.equals(lost.getNetwork()));
     }
 
+    private void registerNetworkCallback(NetworkRequest request, TestableNetworkCallback callback) {
+        sCM.registerNetworkCallback(request, callback);
+        mCallbacksToUnregister.add(callback);
+    }
+
     private class VerifyStartStopVpnProfileTest implements TestNetworkRunnable.Test {
         private final boolean mTestIpv6Only;
         private final boolean mRequiresValidation;
+        private final boolean mTestSessionKey;
 
         /**
          * Constructs the test
          *
          * @param testIpv6Only if true, builds a IPv6-only test; otherwise builds a IPv4-only test
          * @param requiresValidation whether this VPN should request platform validation
+         * @param testSessionKey if true, start VPN by calling startProvisionedVpnProfileSession()
          */
-        VerifyStartStopVpnProfileTest(boolean testIpv6Only, boolean requiresValidation) {
+        VerifyStartStopVpnProfileTest(boolean testIpv6Only, boolean requiresValidation,
+                boolean testSessionKey) {
             mTestIpv6Only = testIpv6Only;
             mRequiresValidation = requiresValidation;
+            mTestSessionKey = testSessionKey;
         }
 
         @Override
@@ -500,7 +585,7 @@
             final IkeTunUtils tunUtils = new IkeTunUtils(testIface.getFileDescriptor());
 
             checkStartStopVpnProfileBuildsNetworks(
-                    tunUtils, mTestIpv6Only, mRequiresValidation);
+                    tunUtils, mTestIpv6Only, mRequiresValidation, mTestSessionKey);
         }
 
         @Override
@@ -524,10 +609,14 @@
 
         // Requires shell permission to update appops.
         runWithShellPermissionIdentity(
-                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(false, false)));
+                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(
+                        false /* testIpv6Only */, false /* requiresValidation */,
+                        false /* testSessionKey */)));
 
         runWithShellPermissionIdentity(
-                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(false, true)));
+                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(
+                        false /* testIpv6Only */, true /* requiresValidation */,
+                        false /* testSessionKey */)));
     }
 
     @Test
@@ -536,9 +625,31 @@
 
         // Requires shell permission to update appops.
         runWithShellPermissionIdentity(
-                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(true, false)));
+                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(
+                        true /* testIpv6Only */, false /* requiresValidation */,
+                        false /* testSessionKey */)));
         runWithShellPermissionIdentity(
-                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(true, true)));
+                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(
+                        true /* testIpv6Only */, true /* requiresValidation */,
+                        false /* testSessionKey */)));
+    }
+
+    @IgnoreUpTo(SC_V2)
+    @Test
+    public void testStartProvisionedVpnProfileSession() throws Exception {
+        assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+        assumeTrue(TestUtils.shouldTestTApis());
+
+        // Requires shell permission to update appops.
+        runWithShellPermissionIdentity(
+                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(
+                        false /* testIpv6Only */, false /* requiresValidation */,
+                        true /* testSessionKey */)));
+
+        runWithShellPermissionIdentity(
+                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(
+                        true /* testIpv6Only */, false /* requiresValidation */,
+                        true /* testSessionKey */)));
     }
 
     private static class CertificateAndKey {
diff --git a/tests/cts/net/util/java/android/net/cts/util/IkeSessionTestUtils.java b/tests/cts/net/util/java/android/net/cts/util/IkeSessionTestUtils.java
new file mode 100644
index 0000000..b4ebcdb
--- /dev/null
+++ b/tests/cts/net/util/java/android/net/cts/util/IkeSessionTestUtils.java
@@ -0,0 +1,59 @@
+/*
+ * 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 android.net.cts.util;
+
+import static android.net.ipsec.ike.SaProposal.KEY_LEN_AES_128;
+import static android.net.ipsec.ike.SaProposal.KEY_LEN_UNUSED;
+
+import android.net.ipsec.ike.ChildSaProposal;
+import android.net.ipsec.ike.IkeFqdnIdentification;
+import android.net.ipsec.ike.IkeSaProposal;
+import android.net.ipsec.ike.IkeSessionParams;
+import android.net.ipsec.ike.SaProposal;
+import android.net.ipsec.ike.TunnelModeChildSessionParams;
+
+/** Shared testing parameters and util methods for testing IKE */
+public class IkeSessionTestUtils {
+    private static final String TEST_CLIENT_ADDR = "test.client.com";
+    private static final String TEST_SERVER_ADDR = "test.server.com";
+    private static final String TEST_SERVER = "2001:0db8:85a3:0000:0000:8a2e:0370:7334";
+
+    public static final IkeSaProposal SA_PROPOSAL = new IkeSaProposal.Builder()
+            .addEncryptionAlgorithm(SaProposal.ENCRYPTION_ALGORITHM_3DES, KEY_LEN_UNUSED)
+            .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA1_96)
+            .addPseudorandomFunction(SaProposal.PSEUDORANDOM_FUNCTION_AES128_XCBC)
+            .addDhGroup(SaProposal.DH_GROUP_1024_BIT_MODP)
+            .build();
+    public static final ChildSaProposal CHILD_PROPOSAL = new ChildSaProposal.Builder()
+            .addEncryptionAlgorithm(SaProposal.ENCRYPTION_ALGORITHM_AES_CBC, KEY_LEN_AES_128)
+            .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_NONE)
+            .addDhGroup(SaProposal.DH_GROUP_1024_BIT_MODP)
+            .build();
+
+    public static final IkeSessionParams IKE_PARAMS =
+            new IkeSessionParams.Builder()
+                    .setServerHostname(TEST_SERVER)
+                    .addSaProposal(SA_PROPOSAL)
+                    .setLocalIdentification(new IkeFqdnIdentification(TEST_CLIENT_ADDR))
+                    .setRemoteIdentification(new IkeFqdnIdentification(TEST_SERVER_ADDR))
+                    .setAuthPsk("psk".getBytes())
+                    .build();
+    public static final TunnelModeChildSessionParams CHILD_PARAMS =
+            new TunnelModeChildSessionParams.Builder()
+                    .addSaProposal(CHILD_PROPOSAL)
+                    .build();
+}
diff --git a/tests/cts/netpermission/internetpermission/Android.bp b/tests/cts/netpermission/internetpermission/Android.bp
new file mode 100644
index 0000000..37ad7cb
--- /dev/null
+++ b/tests/cts/netpermission/internetpermission/Android.bp
@@ -0,0 +1,33 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "CtsNetTestCasesInternetPermission",
+    defaults: ["cts_defaults"],
+
+    srcs: ["src/**/*.java"],
+
+    static_libs: ["ctstestrunner-axt"],
+
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+
+}
diff --git a/tests/cts/netpermission/internetpermission/AndroidManifest.xml b/tests/cts/netpermission/internetpermission/AndroidManifest.xml
new file mode 100644
index 0000000..45ef5bd
--- /dev/null
+++ b/tests/cts/netpermission/internetpermission/AndroidManifest.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="android.networkpermission.internetpermission.cts">
+
+    <application>
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name="android.networkpermission.internetpermission.cts.InternetPermissionTest"
+             android:label="InternetPermissionTest"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
+            </intent-filter>
+        </activity>
+    </application>
+
+    <!--
+                The CTS stubs package cannot be used as the target application here,
+                since that requires many permissions to be set. Instead, specify this
+                package itself as the target and include any stub activities needed.
+
+                This test package uses the default InstrumentationTestRunner, because
+                the InstrumentationCtsTestRunner is only available in the stubs
+                package. That runner cannot be added to this package either, since it
+                relies on hidden APIs.
+            -->
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="android.networkpermission.internetpermission.cts"
+         android:label="CTS tests for INTERNET permissions">
+        <meta-data android:name="listener"
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
+    </instrumentation>
+
+</manifest>
diff --git a/tests/cts/netpermission/internetpermission/AndroidTest.xml b/tests/cts/netpermission/internetpermission/AndroidTest.xml
new file mode 100644
index 0000000..3b23e72
--- /dev/null
+++ b/tests/cts/netpermission/internetpermission/AndroidTest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Config for CTS internet permission test cases">
+    <option name="test-suite-tag" value="cts" />
+    <option name="config-descriptor:metadata" key="component" value="networking" />
+    <option name="config-descriptor:metadata" key="parameter" value="instant_app" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+    <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+    <option name="not-shardable" value="true" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="CtsNetTestCasesInternetPermission.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.networkpermission.internetpermission.cts" />
+        <option name="runtime-hint" value="10s" />
+    </test>
+</configuration>
diff --git a/tests/cts/netpermission/internetpermission/TEST_MAPPING b/tests/cts/netpermission/internetpermission/TEST_MAPPING
new file mode 100644
index 0000000..60877f4
--- /dev/null
+++ b/tests/cts/netpermission/internetpermission/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsNetTestCasesInternetPermission"
+    }
+  ]
+}
diff --git a/tests/cts/netpermission/internetpermission/src/android/net/cts/network/permission/InternetPermissionTest.java b/tests/cts/netpermission/internetpermission/src/android/net/cts/network/permission/InternetPermissionTest.java
new file mode 100644
index 0000000..2b7c8b5
--- /dev/null
+++ b/tests/cts/netpermission/internetpermission/src/android/net/cts/network/permission/InternetPermissionTest.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts.networkpermission.internetpermission;
+
+import static org.junit.Assert.fail;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Socket;
+/**
+* Test that protected android.net.ConnectivityManager methods cannot be called without
+* permissions
+*/
+@RunWith(AndroidJUnit4.class)
+public class InternetPermissionTest {
+
+    /**
+     * Verify that create inet socket failed because of the permission is missing.
+     * <p>Tests Permission:
+     *   {@link android.Manifest.permission#INTERNET}.
+     */
+    @SmallTest
+    @Test
+    public void testCreateSocket() throws Exception {
+        try {
+            Socket socket = new Socket("example.com", 80);
+            fail("Ceate inet socket did not throw SecurityException as expected");
+        } catch (SecurityException e) {
+            // expected
+        }
+    }
+}
diff --git a/tests/cts/netpermission/updatestatspermission/Android.bp b/tests/cts/netpermission/updatestatspermission/Android.bp
new file mode 100644
index 0000000..7a24886
--- /dev/null
+++ b/tests/cts/netpermission/updatestatspermission/Android.bp
@@ -0,0 +1,33 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "CtsNetTestCasesUpdateStatsPermission",
+    defaults: ["cts_defaults"],
+
+    srcs: ["src/**/*.java"],
+
+    static_libs: ["ctstestrunner-axt"],
+
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+
+}
diff --git a/tests/cts/netpermission/updatestatspermission/AndroidManifest.xml b/tests/cts/netpermission/updatestatspermission/AndroidManifest.xml
new file mode 100644
index 0000000..6babe8f
--- /dev/null
+++ b/tests/cts/netpermission/updatestatspermission/AndroidManifest.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="android.networkpermission.updatestatspermission.cts">
+
+    <!--
+                 This CTS test is designed to test that an unprivileged app cannot get the
+                 UPDATE_DEVICE_STATS permission even if it specified it in the manifest. the
+                 UPDATE_DEVICE_STATS permission is a signature|privileged permission that CTS
+                 test cannot have.
+            -->
+    <uses-permission android:name="android.permission.UPDATE_DEVICE_STATS"/>
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <application>
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name="android.networkpermission.updatestatspermission.cts.UpdateStatsPermissionTest"
+             android:label="UpdateStatsPermissionTest"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
+            </intent-filter>
+        </activity>
+    </application>
+
+    <!--
+                The CTS stubs package cannot be used as the target application here,
+                since that requires many permissions to be set. Instead, specify this
+                package itself as the target and include any stub activities needed.
+
+                This test package uses the default InstrumentationTestRunner, because
+                the InstrumentationCtsTestRunner is only available in the stubs
+                package. That runner cannot be added to this package either, since it
+                relies on hidden APIs.
+            -->
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="android.networkpermission.updatestatspermission.cts"
+         android:label="CTS tests for UPDATE_DEVICE_STATS permissions">
+        <meta-data android:name="listener"
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
+    </instrumentation>
+
+</manifest>
diff --git a/tests/cts/netpermission/updatestatspermission/AndroidTest.xml b/tests/cts/netpermission/updatestatspermission/AndroidTest.xml
new file mode 100644
index 0000000..c47cad9
--- /dev/null
+++ b/tests/cts/netpermission/updatestatspermission/AndroidTest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Config for CTS update stats permission test cases">
+    <option name="test-suite-tag" value="cts" />
+    <option name="config-descriptor:metadata" key="component" value="networking" />
+    <option name="config-descriptor:metadata" key="parameter" value="instant_app" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+    <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+    <option name="not-shardable" value="true" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="CtsNetTestCasesUpdateStatsPermission.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.networkpermission.updatestatspermission.cts" />
+        <option name="runtime-hint" value="10s" />
+    </test>
+</configuration>
diff --git a/tests/cts/netpermission/updatestatspermission/TEST_MAPPING b/tests/cts/netpermission/updatestatspermission/TEST_MAPPING
new file mode 100644
index 0000000..6d6dfe0
--- /dev/null
+++ b/tests/cts/netpermission/updatestatspermission/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsNetTestCasesUpdateStatsPermission"
+    }
+  ]
+}
diff --git a/tests/cts/netpermission/updatestatspermission/src/android/net/cts/network/permission/UpdateStatsPermissionTest.java b/tests/cts/netpermission/updatestatspermission/src/android/net/cts/network/permission/UpdateStatsPermissionTest.java
new file mode 100644
index 0000000..bea843c
--- /dev/null
+++ b/tests/cts/netpermission/updatestatspermission/src/android/net/cts/network/permission/UpdateStatsPermissionTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts.networkpermission.updatestatspermission;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.net.TrafficStats;
+import android.os.Process;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.OutputStream;
+import java.net.Socket;
+
+/**
+* Test that protected android.net.ConnectivityManager methods cannot be called without
+* permissions
+*/
+@RunWith(AndroidJUnit4.class)
+public class UpdateStatsPermissionTest {
+
+    /**
+     * Verify that setCounterSet for a different uid failed because of the permission cannot be
+     * granted to a third-party app.
+     * <p>Tests Permission:
+     *   {@link android.Manifest.permission#UPDATE_DEVICE_STATS}.
+     */
+    @SmallTest
+    @Test
+    public void testUpdateDeviceStatsPermission() throws Exception {
+
+        // Set the current thread uid to a another uid. It should silently fail when tagging the
+        // socket since the current process doesn't have UPDATE_DEVICE_STATS permission.
+        TrafficStats.setThreadStatsTag(0);
+        TrafficStats.setThreadStatsUid(/*root uid*/ 0);
+        Socket socket = new Socket("example.com", 80);
+        TrafficStats.tagSocket(socket);
+
+        // Transfer 1K of data to a remote host and verify the stats is still billed to the current
+        // uid.
+        final int byteCount = 1024;
+
+        socket.setTcpNoDelay(true);
+        socket.setSoLinger(true, 0);
+        OutputStream out = socket.getOutputStream();
+        byte[] buf = new byte[byteCount];
+        final long uidTxBytesBefore = TrafficStats.getUidTxBytes(Process.myUid());
+        out.write(buf);
+        out.close();
+        socket.close();
+        long uidTxBytesAfter = TrafficStats.getUidTxBytes(Process.myUid());
+        long uidTxDeltaBytes = uidTxBytesAfter - uidTxBytesBefore;
+        assertTrue("uidtxb: " + uidTxBytesBefore + " -> " + uidTxBytesAfter + " delta="
+                + uidTxDeltaBytes + " >= " + byteCount, uidTxDeltaBytes >= byteCount);
+    }
+
+    static final int UNSUPPORTED = -1;
+
+    /**
+     * Verify that get TrafficStats of a different uid failed because of the permission is not
+     * granted to a third-party app.
+     * <p>Tests Permission:
+     *   {@link android.Manifest.permission#UPDATE_DEVICE_STATS}.
+     */
+    @SmallTest
+    @Test
+    public void testGetStatsOfOtherUid() throws Exception {
+        // Test get stats of another uid failed since the current process does not have permission
+        assertEquals(UNSUPPORTED, TrafficStats.getUidRxBytes(/*root uid*/ 0));
+    }
+}
diff --git a/tests/ethernet/Android.bp b/tests/ethernet/Android.bp
deleted file mode 100644
index 8342490..0000000
--- a/tests/ethernet/Android.bp
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright (C) 2018 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-package {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-// TODO: merge the tests into service-connectivity tests after
-// ethernet service migration completes. So far just import the
-// ethernet service source to fix the dependencies.
-android_test {
-    name: "EthernetServiceTests",
-
-    srcs: [
-        ":ethernet-service-test-sources",
-        "java/**/*.java",
-    ],
-
-    certificate: "platform",
-    platform_apis: true,
-
-    libs: [
-        "android.test.runner",
-        "android.test.base",
-        "android.test.mock",
-        "framework-connectivity.impl",
-        "framework-connectivity-t.impl",
-        "ServiceConnectivityResources",
-    ],
-
-    static_libs: [
-        "androidx.test.rules",
-        "frameworks-base-testutils",
-        "mockito-target-minus-junit4",
-        "net-tests-utils",
-        "services.core",
-        "services.net",
-    ],
-    test_suites: ["general-tests"],
-}
diff --git a/tests/ethernet/AndroidManifest.xml b/tests/ethernet/AndroidManifest.xml
deleted file mode 100644
index cd875b0..0000000
--- a/tests/ethernet/AndroidManifest.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2018 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License
-  -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.server.ethernet.tests">
-
-    <application>
-        <uses-library android:name="android.test.runner" />
-    </application>
-
-    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.server.ethernet.tests"
-        android:label="Ethernet Service Tests" />
-</manifest>
diff --git a/tests/native/Android.bp b/tests/native/Android.bp
new file mode 100644
index 0000000..a8d908a
--- /dev/null
+++ b/tests/native/Android.bp
@@ -0,0 +1,33 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_test {
+    name: "connectivity_native_test",
+    test_suites: [
+        "general-tests",
+        "mts-tethering",
+        "vts",
+    ],
+    test_config_template: "AndroidTestTemplate.xml",
+    min_sdk_version: "31",
+    tidy: false,
+    srcs: [
+        "connectivity_native_test.cpp",
+    ],
+    header_libs: ["bpf_connectivity_headers"],
+    shared_libs: [
+        "libbase",
+        "libbinder_ndk",
+        "liblog",
+        "libnetutils",
+        "libprocessgroup",
+    ],
+    static_libs: [
+        "connectivity_native_aidl_interface-lateststable-ndk",
+        "libcutils",
+        "libmodules-utils-build",
+        "libutils",
+    ],
+    compile_multilib: "first",
+}
diff --git a/tests/native/AndroidTestTemplate.xml b/tests/native/AndroidTestTemplate.xml
new file mode 100644
index 0000000..44e35a9
--- /dev/null
+++ b/tests/native/AndroidTestTemplate.xml
@@ -0,0 +1,30 @@
+<!-- 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.
+-->
+<configuration description="Configuration for connectivity {MODULE} tests">
+    <option name="test-suite-tag" value="mts" />
+    <option name="config-descriptor:metadata" key="mainline-param" value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex" />
+    <!-- The tested code is only part of a SDK 30+ module (Tethering) -->
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.Sdk30ModuleController" />
+
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+        <option name="cleanup" value="true" />
+        <option name="push" value="{MODULE}->/data/local/tmp/{MODULE}" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.GTest" >
+        <option name="native-test-device-path" value="/data/local/tmp" />
+        <option name="module-name" value="{MODULE}" />
+    </test>
+</configuration>
diff --git a/tests/native/OWNERS b/tests/native/OWNERS
new file mode 100644
index 0000000..8dfa455
--- /dev/null
+++ b/tests/native/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 31808
+set noparent
+file:platform/packages/modules/Connectivity:master:/OWNERS_core_networking_xts
diff --git a/tests/native/connectivity_native_test.cpp b/tests/native/connectivity_native_test.cpp
new file mode 100644
index 0000000..3db5265
--- /dev/null
+++ b/tests/native/connectivity_native_test.cpp
@@ -0,0 +1,286 @@
+/*
+ * 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.
+ */
+
+#include <aidl/android/net/connectivity/aidl/ConnectivityNative.h>
+#include <android/binder_manager.h>
+#include <android/binder_process.h>
+#include <android-modules-utils/sdk_level.h>
+#include <cutils/misc.h>  // FIRST_APPLICATION_UID
+#include <gtest/gtest.h>
+#include <netinet/in.h>
+
+#include "bpf/BpfUtils.h"
+
+using aidl::android::net::connectivity::aidl::IConnectivityNative;
+
+class ConnectivityNativeBinderTest : public ::testing::Test {
+  public:
+    std::vector<int32_t> mActualBlockedPorts;
+
+    ConnectivityNativeBinderTest() {
+        AIBinder* binder = AServiceManager_getService("connectivity_native");
+        ndk::SpAIBinder sBinder = ndk::SpAIBinder(binder);
+        mService = aidl::android::net::connectivity::aidl::IConnectivityNative::fromBinder(sBinder);
+    }
+
+    void SetUp() override {
+        // Skip test case if not on T.
+        if (!android::modules::sdklevel::IsAtLeastT()) GTEST_SKIP() <<
+                "Should be at least T device.";
+
+        // Skip test case if not on 5.4 kernel which is required by bpf prog.
+        if (!android::bpf::isAtLeastKernelVersion(5, 4, 0)) GTEST_SKIP() <<
+                "Kernel should be at least 5.4.";
+
+        ASSERT_NE(nullptr, mService.get());
+
+        // If there are already ports being blocked on device unblockAllPortsForBind() store
+        // the currently blocked ports and add them back at the end of the test. Do this for
+        // every test case so additional test cases do not forget to add ports back.
+        ndk::ScopedAStatus status = mService->getPortsBlockedForBind(&mActualBlockedPorts);
+        EXPECT_TRUE(status.isOk()) << status.getDescription ();
+
+    }
+
+    void TearDown() override {
+        ndk::ScopedAStatus status;
+        if (mActualBlockedPorts.size() > 0) {
+            for (int i : mActualBlockedPorts) {
+                mService->blockPortForBind(i);
+                EXPECT_TRUE(status.isOk()) << status.getDescription ();
+            }
+        }
+    }
+
+  protected:
+    std::shared_ptr<IConnectivityNative> mService;
+
+    void runSocketTest (sa_family_t family, const int type, bool blockPort) {
+        ndk::ScopedAStatus status;
+        in_port_t port = 0;
+        int sock, sock2;
+        // Open two sockets with SO_REUSEADDR and expect they can both bind to port.
+        sock = openSocket(&port, family, type, false /* expectBindFail */);
+        sock2 = openSocket(&port, family, type, false /* expectBindFail */);
+
+        int blockedPort = 0;
+        if (blockPort) {
+            blockedPort = ntohs(port);
+            status = mService->blockPortForBind(blockedPort);
+            EXPECT_TRUE(status.isOk()) << status.getDescription ();
+        }
+
+        int sock3 = openSocket(&port, family, type, blockPort /* expectBindFail */);
+
+        if (blockPort) {
+            EXPECT_EQ(-1, sock3);
+            status = mService->unblockPortForBind(blockedPort);
+            EXPECT_TRUE(status.isOk()) << status.getDescription ();
+        } else {
+            EXPECT_NE(-1, sock3);
+        }
+
+        close(sock);
+        close(sock2);
+        close(sock3);
+    }
+
+    /*
+    * Open the socket and update the port.
+    */
+    int openSocket(in_port_t* port, sa_family_t family, const int type, bool expectBindFail) {
+        int ret = 0;
+        int enable = 1;
+        const int sock = socket(family, type, 0);
+        ret = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable));
+        EXPECT_EQ(0, ret);
+
+        if (family == AF_INET) {
+            struct sockaddr_in addr4 = { .sin_family = family, .sin_port = htons(*port) };
+            ret = bind(sock, (struct sockaddr*) &addr4, sizeof(addr4));
+        } else {
+            struct sockaddr_in6 addr6 = { .sin6_family = family, .sin6_port = htons(*port) };
+            ret = bind(sock, (struct sockaddr*) &addr6, sizeof(addr6));
+        }
+
+        if (expectBindFail) {
+            EXPECT_NE(0, ret);
+            // If port is blocked, return here since the port is not needed
+            // for subsequent sockets.
+            close(sock);
+            return -1;
+        }
+        EXPECT_EQ(0, ret) << "bind unexpectedly failed, errno: " << errno;
+
+        if (family == AF_INET) {
+            struct sockaddr_in sin;
+            socklen_t len = sizeof(sin);
+            EXPECT_NE(-1, getsockname(sock, (struct sockaddr *)&sin, &len));
+            EXPECT_NE(0, ntohs(sin.sin_port));
+            if (*port != 0) EXPECT_EQ(*port, ntohs(sin.sin_port));
+            *port = ntohs(sin.sin_port);
+        } else {
+            struct sockaddr_in6 sin;
+            socklen_t len = sizeof(sin);
+            EXPECT_NE(-1, getsockname(sock, (struct sockaddr *)&sin, &len));
+            EXPECT_NE(0, ntohs(sin.sin6_port));
+            if (*port != 0) EXPECT_EQ(*port, ntohs(sin.sin6_port));
+            *port = ntohs(sin.sin6_port);
+        }
+        return sock;
+    }
+};
+
+TEST_F(ConnectivityNativeBinderTest, PortUnblockedV4Udp) {
+    runSocketTest(AF_INET, SOCK_DGRAM, false);
+}
+
+TEST_F(ConnectivityNativeBinderTest, PortUnblockedV4Tcp) {
+    runSocketTest(AF_INET, SOCK_STREAM, false);
+}
+
+TEST_F(ConnectivityNativeBinderTest, PortUnblockedV6Udp) {
+    runSocketTest(AF_INET6, SOCK_DGRAM, false);
+}
+
+TEST_F(ConnectivityNativeBinderTest, PortUnblockedV6Tcp) {
+    runSocketTest(AF_INET6, SOCK_STREAM, false);
+}
+
+TEST_F(ConnectivityNativeBinderTest, BlockPort4Udp) {
+    runSocketTest(AF_INET, SOCK_DGRAM, true);
+}
+
+TEST_F(ConnectivityNativeBinderTest, BlockPort4Tcp) {
+    runSocketTest(AF_INET, SOCK_STREAM, true);
+}
+
+TEST_F(ConnectivityNativeBinderTest, BlockPort6Udp) {
+    runSocketTest(AF_INET6, SOCK_DGRAM, true);
+}
+
+TEST_F(ConnectivityNativeBinderTest, BlockPort6Tcp) {
+    runSocketTest(AF_INET6, SOCK_STREAM, true);
+}
+
+TEST_F(ConnectivityNativeBinderTest, BlockPortTwice) {
+    ndk::ScopedAStatus status = mService->blockPortForBind(5555);
+    EXPECT_TRUE(status.isOk()) << status.getDescription ();
+    status = mService->blockPortForBind(5555);
+    EXPECT_TRUE(status.isOk()) << status.getDescription ();
+    status = mService->unblockPortForBind(5555);
+    EXPECT_TRUE(status.isOk()) << status.getDescription ();
+}
+
+TEST_F(ConnectivityNativeBinderTest, GetBlockedPorts) {
+    ndk::ScopedAStatus status;
+    std::vector<int> blockedPorts{1, 100, 1220, 1333, 2700, 5555, 5600, 65000};
+    for (int i : blockedPorts) {
+        status = mService->blockPortForBind(i);
+        EXPECT_TRUE(status.isOk()) << status.getDescription ();
+    }
+    std::vector<int32_t> actualBlockedPorts;
+    status = mService->getPortsBlockedForBind(&actualBlockedPorts);
+    EXPECT_TRUE(status.isOk()) << status.getDescription ();
+    EXPECT_FALSE(actualBlockedPorts.empty());
+    EXPECT_EQ(blockedPorts, actualBlockedPorts);
+
+    // Remove the ports we added.
+    status = mService->unblockAllPortsForBind();
+    EXPECT_TRUE(status.isOk()) << status.getDescription ();
+    status = mService->getPortsBlockedForBind(&actualBlockedPorts);
+    EXPECT_TRUE(status.isOk()) << status.getDescription ();
+    EXPECT_TRUE(actualBlockedPorts.empty());
+}
+
+TEST_F(ConnectivityNativeBinderTest, UnblockAllPorts) {
+    ndk::ScopedAStatus status;
+    std::vector<int> blockedPorts{1, 100, 1220, 1333, 2700, 5555, 5600, 65000};
+
+    if (mActualBlockedPorts.size() > 0) {
+        status = mService->unblockAllPortsForBind();
+    }
+
+    for (int i : blockedPorts) {
+        status = mService->blockPortForBind(i);
+        EXPECT_TRUE(status.isOk()) << status.getDescription ();
+    }
+
+    std::vector<int32_t> actualBlockedPorts;
+    status = mService->getPortsBlockedForBind(&actualBlockedPorts);
+    EXPECT_TRUE(status.isOk()) << status.getDescription ();
+    EXPECT_FALSE(actualBlockedPorts.empty());
+
+    status = mService->unblockAllPortsForBind();
+    EXPECT_TRUE(status.isOk()) << status.getDescription ();
+    status = mService->getPortsBlockedForBind(&actualBlockedPorts);
+    EXPECT_TRUE(status.isOk()) << status.getDescription ();
+    EXPECT_TRUE(actualBlockedPorts.empty());
+    // If mActualBlockedPorts is not empty, ports will be added back in teardown.
+}
+
+TEST_F(ConnectivityNativeBinderTest, BlockNegativePort) {
+    int retry = 0;
+    ndk::ScopedAStatus status;
+    do {
+        status = mService->blockPortForBind(-1);
+        // TODO: find out why transaction failed is being thrown on the first attempt.
+    } while (status.getExceptionCode() == EX_TRANSACTION_FAILED && retry++ < 5);
+    EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode());
+}
+
+TEST_F(ConnectivityNativeBinderTest, UnblockNegativePort) {
+    int retry = 0;
+    ndk::ScopedAStatus status;
+    do {
+        status = mService->unblockPortForBind(-1);
+        // TODO: find out why transaction failed is being thrown on the first attempt.
+    } while (status.getExceptionCode() == EX_TRANSACTION_FAILED && retry++ < 5);
+    EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode());
+}
+
+TEST_F(ConnectivityNativeBinderTest, BlockMaxPort) {
+    int retry = 0;
+    ndk::ScopedAStatus status;
+    do {
+        status = mService->blockPortForBind(65536);
+        // TODO: find out why transaction failed is being thrown on the first attempt.
+    } while (status.getExceptionCode() == EX_TRANSACTION_FAILED && retry++ < 5);
+    EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode());
+}
+
+TEST_F(ConnectivityNativeBinderTest, UnblockMaxPort) {
+    int retry = 0;
+    ndk::ScopedAStatus status;
+    do {
+        status = mService->unblockPortForBind(65536);
+        // TODO: find out why transaction failed is being thrown on the first attempt.
+    } while (status.getExceptionCode() == EX_TRANSACTION_FAILED && retry++ < 5);
+    EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode());
+}
+
+TEST_F(ConnectivityNativeBinderTest, CheckPermission) {
+    int retry = 0;
+    int curUid = getuid();
+    EXPECT_EQ(0, seteuid(FIRST_APPLICATION_UID + 2000)) << "seteuid failed: " << strerror(errno);
+    ndk::ScopedAStatus status;
+    do {
+        status = mService->blockPortForBind(5555);
+        // TODO: find out why transaction failed is being thrown on the first attempt.
+    } while (status.getExceptionCode() == EX_TRANSACTION_FAILED && retry++ < 5);
+    EXPECT_EQ(EX_SECURITY, status.getExceptionCode());
+    EXPECT_EQ(0, seteuid(curUid)) << "seteuid failed: " << strerror(errno);
+}
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 07dcae3..5b926de 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -63,6 +63,7 @@
         "java/android/net/IpSecManagerTest.java",
         "java/android/net/IpSecTransformTest.java",
         "java/android/net/KeepalivePacketDataUtilTest.java",
+        "java/android/net/NetworkIdentitySetTest.kt",
         "java/android/net/NetworkIdentityTest.kt",
         "java/android/net/NetworkStats*.java",
         "java/android/net/NetworkTemplateTest.kt",
@@ -83,8 +84,10 @@
         "java/com/android/server/connectivity/MultipathPolicyTrackerTest.java",
         "java/com/android/server/connectivity/NetdEventListenerServiceTest.java",
         "java/com/android/server/connectivity/VpnTest.java",
+        "java/com/android/server/ethernet/*.java",
         "java/com/android/server/net/ipmemorystore/*.java",
         "java/com/android/server/net/BpfInterfaceMapUpdaterTest.java",
+        "java/com/android/server/net/IpConfigStoreTest.java",
         "java/com/android/server/net/NetworkStats*.java",
         "java/com/android/server/net/TestableUsageCallback.kt",
     ]
@@ -128,6 +131,7 @@
         "service-connectivity-pre-jarjar",
         "service-connectivity-tiramisu-pre-jarjar",
         "services.core-vpn",
+        "cts-net-utils"
     ],
     libs: [
         "android.net.ipsec.ike.stubs.module_lib",
diff --git a/tests/unit/java/android/net/Ikev2VpnProfileTest.java b/tests/unit/java/android/net/Ikev2VpnProfileTest.java
index 8559c20..8222ca1 100644
--- a/tests/unit/java/android/net/Ikev2VpnProfileTest.java
+++ b/tests/unit/java/android/net/Ikev2VpnProfileTest.java
@@ -16,6 +16,9 @@
 
 package android.net;
 
+import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS;
+import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS;
+
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
 import static org.junit.Assert.assertArrayEquals;
@@ -25,6 +28,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import android.net.ipsec.ike.IkeTunnelConnectionParams;
 import android.os.Build;
 import android.test.mock.MockContext;
 
@@ -441,6 +445,33 @@
         assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
     }
 
+    @Test
+    public void testConversionIsLosslessWithIkeTunConnParams() throws Exception {
+        final IkeTunnelConnectionParams tunnelParams =
+                new IkeTunnelConnectionParams(IKE_PARAMS, CHILD_PARAMS);
+        // Config authentication related fields is not required while building with
+        // IkeTunnelConnectionParams.
+        final Ikev2VpnProfile ikeProfile = new Ikev2VpnProfile.Builder(tunnelParams).build();
+        assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
+    }
+
+    @Test
+    public void testEquals() throws Exception {
+        // Verify building without IkeTunnelConnectionParams
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
+        assertEquals(builder.build(), builder.build());
+
+        // Verify building with IkeTunnelConnectionParams
+        final IkeTunnelConnectionParams tunnelParams =
+                new IkeTunnelConnectionParams(IKE_PARAMS, CHILD_PARAMS);
+        final IkeTunnelConnectionParams tunnelParams2 =
+                new IkeTunnelConnectionParams(IKE_PARAMS, CHILD_PARAMS);
+        assertEquals(new Ikev2VpnProfile.Builder(tunnelParams).build(),
+                new Ikev2VpnProfile.Builder(tunnelParams2).build());
+    }
+
+
     private static class CertificateAndKey {
         public final X509Certificate cert;
         public final PrivateKey key;
diff --git a/tests/unit/java/android/net/NetworkIdentitySetTest.kt b/tests/unit/java/android/net/NetworkIdentitySetTest.kt
new file mode 100644
index 0000000..d61ebf9
--- /dev/null
+++ b/tests/unit/java/android/net/NetworkIdentitySetTest.kt
@@ -0,0 +1,66 @@
+/*
+ * 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 android.net
+
+import android.content.Context
+import android.net.ConnectivityManager.TYPE_MOBILE
+import android.os.Build
+import android.telephony.TelephonyManager
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import kotlin.test.assertEquals
+
+private const val TEST_IMSI1 = "testimsi1"
+
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+@RunWith(DevSdkIgnoreRunner::class)
+class NetworkIdentitySetTest {
+    private val mockContext = mock(Context::class.java)
+
+    private fun buildMobileNetworkStateSnapshot(
+        caps: NetworkCapabilities,
+        subscriberId: String
+    ): NetworkStateSnapshot {
+        return NetworkStateSnapshot(mock(Network::class.java), caps,
+                LinkProperties(), subscriberId, TYPE_MOBILE)
+    }
+
+    @Test
+    fun testCompare() {
+        val ident1 = NetworkIdentity.buildNetworkIdentity(mockContext,
+            buildMobileNetworkStateSnapshot(NetworkCapabilities(), TEST_IMSI1),
+            false /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS)
+        val ident2 = NetworkIdentity.buildNetworkIdentity(mockContext,
+            buildMobileNetworkStateSnapshot(NetworkCapabilities(), TEST_IMSI1),
+            true /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS)
+
+        // Verify that the results of comparing two empty sets are equal
+        assertEquals(0, NetworkIdentitySet.compare(NetworkIdentitySet(), NetworkIdentitySet()))
+
+        val identSet1 = NetworkIdentitySet()
+        val identSet2 = NetworkIdentitySet()
+        identSet1.add(ident1)
+        identSet2.add(ident2)
+        assertEquals(-1, NetworkIdentitySet.compare(NetworkIdentitySet(), identSet1))
+        assertEquals(1, NetworkIdentitySet.compare(identSet1, NetworkIdentitySet()))
+        assertEquals(0, NetworkIdentitySet.compare(identSet1, identSet1))
+        assertEquals(-1, NetworkIdentitySet.compare(identSet1, identSet2))
+    }
+}
diff --git a/tests/unit/java/com/android/internal/net/VpnProfileTest.java b/tests/unit/java/com/android/internal/net/VpnProfileTest.java
index 943a559..360390d 100644
--- a/tests/unit/java/com/android/internal/net/VpnProfileTest.java
+++ b/tests/unit/java/com/android/internal/net/VpnProfileTest.java
@@ -16,6 +16,9 @@
 
 package com.android.internal.net;
 
+import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS;
+import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS;
+
 import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
 import static com.android.testutils.ParcelUtils.assertParcelSane;
 
@@ -26,6 +29,7 @@
 import static org.junit.Assert.assertTrue;
 
 import android.net.IpSecAlgorithm;
+import android.net.ipsec.ike.IkeTunnelConnectionParams;
 import android.os.Build;
 
 import androidx.test.filters.SmallTest;
@@ -85,7 +89,8 @@
 
     private VpnProfile getSampleIkev2Profile(String key) {
         final VpnProfile p = new VpnProfile(key, true /* isRestrictedToTestNetworks */,
-                false /* excludesLocalRoutes */, true /* requiresPlatformValidation */);
+                false /* excludesLocalRoutes */, true /* requiresPlatformValidation */,
+                null /* ikeTunConnParams */);
 
         p.name = "foo";
         p.type = VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS;
@@ -120,6 +125,35 @@
         return p;
     }
 
+    private VpnProfile getSampleIkev2ProfileWithIkeTunConnParams(String key) {
+        final VpnProfile p = new VpnProfile(key, true /* isRestrictedToTestNetworks */,
+                false /* excludesLocalRoutes */, true /* requiresPlatformValidation */,
+                new IkeTunnelConnectionParams(IKE_PARAMS, CHILD_PARAMS));
+
+        p.name = "foo";
+        p.server = "bar";
+        p.dnsServers = "8.8.8.8";
+        p.searchDomains = "";
+        p.routes = "0.0.0.0/0";
+        p.mppe = false;
+        p.proxy = null;
+        p.setAllowedAlgorithms(
+                Arrays.asList(
+                        IpSecAlgorithm.AUTH_CRYPT_AES_GCM,
+                        IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305,
+                        IpSecAlgorithm.AUTH_HMAC_SHA512,
+                        IpSecAlgorithm.CRYPT_AES_CBC));
+        p.isBypassable = true;
+        p.isMetered = true;
+        p.maxMtu = 1350;
+        p.areAuthParamsInline = true;
+
+        // Not saved, but also not compared.
+        p.saveLogin = true;
+
+        return p;
+    }
+
     @Test
     public void testEquals() {
         assertEquals(
@@ -134,13 +168,21 @@
     public void testParcelUnparcel() {
         if (isAtLeastT()) {
             // excludeLocalRoutes, requiresPlatformValidation were added in T.
-            assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 25);
+            assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 26);
+            assertParcelSane(getSampleIkev2ProfileWithIkeTunConnParams(DUMMY_PROFILE_KEY), 26);
         } else {
             assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 23);
         }
     }
 
     @Test
+    public void testEncodeDecodeWithIkeTunConnParams() {
+        final VpnProfile profile = getSampleIkev2ProfileWithIkeTunConnParams(DUMMY_PROFILE_KEY);
+        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode());
+        assertEquals(profile, decoded);
+    }
+
+    @Test
     public void testEncodeDecode() {
         final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
         final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode());
diff --git a/tests/unit/java/com/android/server/NativeDaemonConnectorTest.java b/tests/unit/java/com/android/server/NativeDaemonConnectorTest.java
deleted file mode 100644
index e2253a2..0000000
--- a/tests/unit/java/com/android/server/NativeDaemonConnectorTest.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright (C) 2011 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 com.android.server.NativeDaemonConnector.appendEscaped;
-import static com.android.server.NativeDaemonConnector.makeCommand;
-
-import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.MediumTest;
-
-import com.android.server.NativeDaemonConnector.SensitiveArg;
-
-/**
- * Tests for {@link NativeDaemonConnector}.
- */
-@MediumTest
-public class NativeDaemonConnectorTest extends AndroidTestCase {
-    private static final String TAG = "NativeDaemonConnectorTest";
-
-    public void testArgumentNormal() throws Exception {
-        final StringBuilder builder = new StringBuilder();
-
-        builder.setLength(0);
-        appendEscaped(builder, "");
-        assertEquals("", builder.toString());
-
-        builder.setLength(0);
-        appendEscaped(builder, "foo");
-        assertEquals("foo", builder.toString());
-
-        builder.setLength(0);
-        appendEscaped(builder, "foo\"bar");
-        assertEquals("foo\\\"bar", builder.toString());
-
-        builder.setLength(0);
-        appendEscaped(builder, "foo\\bar\\\"baz");
-        assertEquals("foo\\\\bar\\\\\\\"baz", builder.toString());
-    }
-
-    public void testArgumentWithSpaces() throws Exception {
-        final StringBuilder builder = new StringBuilder();
-
-        builder.setLength(0);
-        appendEscaped(builder, "foo bar");
-        assertEquals("\"foo bar\"", builder.toString());
-
-        builder.setLength(0);
-        appendEscaped(builder, "foo\"bar\\baz foo");
-        assertEquals("\"foo\\\"bar\\\\baz foo\"", builder.toString());
-    }
-
-    public void testArgumentWithUtf() throws Exception {
-        final StringBuilder builder = new StringBuilder();
-
-        builder.setLength(0);
-        appendEscaped(builder, "caf\u00E9 c\u00F6ffee");
-        assertEquals("\"caf\u00E9 c\u00F6ffee\"", builder.toString());
-    }
-
-    public void testSensitiveArgs() throws Exception {
-        final StringBuilder rawBuilder = new StringBuilder();
-        final StringBuilder logBuilder = new StringBuilder();
-
-        rawBuilder.setLength(0);
-        logBuilder.setLength(0);
-        makeCommand(rawBuilder, logBuilder, 1, "foo", "bar", "baz");
-        assertEquals("1 foo bar baz\0", rawBuilder.toString());
-        assertEquals("1 foo bar baz", logBuilder.toString());
-
-        rawBuilder.setLength(0);
-        logBuilder.setLength(0);
-        makeCommand(rawBuilder, logBuilder, 1, "foo", new SensitiveArg("bar"), "baz");
-        assertEquals("1 foo bar baz\0", rawBuilder.toString());
-        assertEquals("1 foo [scrubbed] baz", logBuilder.toString());
-
-        rawBuilder.setLength(0);
-        logBuilder.setLength(0);
-        makeCommand(rawBuilder, logBuilder, 1, "foo", new SensitiveArg("foo bar"), "baz baz",
-                new SensitiveArg("wat"));
-        assertEquals("1 foo \"foo bar\" \"baz baz\" wat\0", rawBuilder.toString());
-        assertEquals("1 foo [scrubbed] \"baz baz\" [scrubbed]", logBuilder.toString());
-    }
-}
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 5086943..3c228d0 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -19,10 +19,12 @@
 import static libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
 import static libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
 
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -36,6 +38,7 @@
 import android.content.Context;
 import android.net.nsd.INsdManagerCallback;
 import android.net.nsd.INsdServiceConnector;
+import android.net.nsd.MDnsManager;
 import android.net.nsd.NsdManager;
 import android.net.nsd.NsdServiceInfo;
 import android.os.Binder;
@@ -49,9 +52,6 @@
 import androidx.annotation.NonNull;
 import androidx.test.filters.SmallTest;
 
-import com.android.server.NsdService.DaemonConnection;
-import com.android.server.NsdService.DaemonConnectionSupplier;
-import com.android.server.NsdService.NativeCallbackReceiver;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.HandlerUtils;
@@ -63,10 +63,8 @@
 import org.junit.rules.TestRule;
 import org.junit.runner.RunWith;
 import org.mockito.AdditionalAnswers;
-import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
-import org.mockito.Spy;
 
 import java.util.LinkedList;
 import java.util.Queue;
@@ -92,8 +90,7 @@
     public TestRule compatChangeRule = new PlatformCompatChangeRule();
     @Mock Context mContext;
     @Mock ContentResolver mResolver;
-    NativeCallbackReceiver mDaemonCallback;
-    @Spy DaemonConnection mDaemon = new DaemonConnection(mDaemonCallback);
+    @Mock MDnsManager mMockMDnsM;
     HandlerThread mThread;
     TestHandler mHandler;
 
@@ -112,9 +109,17 @@
         MockitoAnnotations.initMocks(this);
         mThread = new HandlerThread("mock-service-handler");
         mThread.start();
-        doReturn(true).when(mDaemon).execute(any());
         mHandler = new TestHandler(mThread.getLooper());
         when(mContext.getContentResolver()).thenReturn(mResolver);
+        doReturn(MDnsManager.MDNS_SERVICE).when(mContext)
+                .getSystemServiceName(MDnsManager.class);
+        doReturn(mMockMDnsM).when(mContext).getSystemService(MDnsManager.MDNS_SERVICE);
+        doReturn(true).when(mMockMDnsM).registerService(
+                anyInt(), anyString(), anyString(), anyInt(), any(), anyInt());
+        doReturn(true).when(mMockMDnsM).stopOperation(anyInt());
+        doReturn(true).when(mMockMDnsM).discover(anyInt(), anyString(), anyInt());
+        doReturn(true).when(mMockMDnsM).resolve(
+                anyInt(), anyString(), anyString(), anyString(), anyInt());
     }
 
     @After
@@ -135,24 +140,25 @@
         waitForIdle();
         final INsdManagerCallback cb1 = getCallback();
         final IBinder.DeathRecipient deathRecipient1 = verifyLinkToDeath(cb1);
-        verify(mDaemon, times(1)).maybeStart();
-        verifyDaemonCommands("start-service");
+        verify(mMockMDnsM, times(1)).registerEventListener(any());
+        verify(mMockMDnsM, times(1)).startDaemon();
 
         connectClient(service);
         waitForIdle();
         final INsdManagerCallback cb2 = getCallback();
         final IBinder.DeathRecipient deathRecipient2 = verifyLinkToDeath(cb2);
-        verify(mDaemon, times(1)).maybeStart();
+        // Daemon has been started, it should not try to start it again.
+        verify(mMockMDnsM, times(1)).registerEventListener(any());
+        verify(mMockMDnsM, times(1)).startDaemon();
 
         deathRecipient1.binderDied();
         // Still 1 client remains, daemon shouldn't be stopped.
         waitForIdle();
-        verify(mDaemon, never()).maybeStop();
+        verify(mMockMDnsM, never()).stopDaemon();
 
         deathRecipient2.binderDied();
         // All clients are disconnected, the daemon should be stopped.
         verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
-        verifyDaemonCommands("stop-service");
     }
 
     @Test
@@ -160,28 +166,30 @@
     public void testNoDaemonStartedWhenClientsConnect() throws Exception {
         final NsdService service = makeService();
 
-        // Creating an NsdManager will not cause any cmds executed, which means
-        // no daemon is started.
+        // Creating an NsdManager will not cause daemon startup.
         connectClient(service);
         waitForIdle();
-        verify(mDaemon, never()).execute(any());
+        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 any cmds executed.
+        // Creating another NsdManager will not cause daemon startup either.
         connectClient(service);
         waitForIdle();
-        verify(mDaemon, never()).execute(any());
+        verify(mMockMDnsM, never()).registerEventListener(any());
+        verify(mMockMDnsM, never()).startDaemon();
         final INsdManagerCallback cb2 = getCallback();
         final IBinder.DeathRecipient deathRecipient2 = verifyLinkToDeath(cb2);
 
-        // If there is no active request, try to clean up the daemon
-        // every time the client disconnects.
+        // If there is no active request, try to clean up the daemon but should not do it because
+        // daemon has not been started.
         deathRecipient1.binderDied();
-        verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
-        reset(mDaemon);
+        verify(mMockMDnsM, never()).unregisterEventListener(any());
+        verify(mMockMDnsM, never()).stopDaemon();
         deathRecipient2.binderDied();
-        verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
+        verify(mMockMDnsM, never()).unregisterEventListener(any());
+        verify(mMockMDnsM, never()).stopDaemon();
     }
 
     private IBinder.DeathRecipient verifyLinkToDeath(INsdManagerCallback cb)
@@ -200,8 +208,8 @@
         waitForIdle();
         final INsdManagerCallback cb1 = getCallback();
         final IBinder.DeathRecipient deathRecipient = verifyLinkToDeath(cb1);
-        verify(mDaemon, never()).maybeStart();
-        verify(mDaemon, never()).execute(any());
+        verify(mMockMDnsM, never()).registerEventListener(any());
+        verify(mMockMDnsM, never()).startDaemon();
 
         NsdServiceInfo request = new NsdServiceInfo("a_name", "a_type");
         request.setPort(2201);
@@ -210,29 +218,31 @@
         NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class);
         client.registerService(request, PROTOCOL, listener1);
         waitForIdle();
-        verify(mDaemon, times(1)).maybeStart();
-        verifyDaemonCommands("start-service", "register 2 a_name a_type 2201");
+        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));
 
         // Client discovery request
         NsdManager.DiscoveryListener listener2 = mock(NsdManager.DiscoveryListener.class);
         client.discoverServices("a_type", PROTOCOL, listener2);
         waitForIdle();
-        verify(mDaemon, times(1)).maybeStart();
-        verifyDaemonCommand("discover 3 a_type 0");
+        verify(mMockMDnsM, times(1)).discover(eq(3), eq("a_type"), eq(0));
 
         // Client resolve request
         NsdManager.ResolveListener listener3 = mock(NsdManager.ResolveListener.class);
         client.resolveService(request, listener3);
         waitForIdle();
-        verify(mDaemon, times(1)).maybeStart();
-        verifyDaemonCommand("resolve 4 a_name a_type local. 0");
+        verify(mMockMDnsM, times(1)).resolve(
+                eq(4), eq("a_name"), eq("a_type"), eq("local."), eq(0));
 
         // Client disconnects, stop the daemon after CLEANUP_DELAY_MS.
         deathRecipient.binderDied();
         verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
         // checks that request are cleaned
-        verifyDaemonCommands("stop-register 2", "stop-discover 3",
-                "stop-resolve 4", "stop-service");
+        verify(mMockMDnsM, times(1)).stopOperation(eq(2));
+        verify(mMockMDnsM, times(1)).stopOperation(eq(3));
+        verify(mMockMDnsM, times(1)).stopOperation(eq(4));
     }
 
     @Test
@@ -246,20 +256,23 @@
         NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class);
         client.registerService(request, PROTOCOL, listener1);
         waitForIdle();
-        verify(mDaemon, times(1)).maybeStart();
+        verify(mMockMDnsM, times(1)).registerEventListener(any());
+        verify(mMockMDnsM, times(1)).startDaemon();
         final INsdManagerCallback cb1 = getCallback();
         final IBinder.DeathRecipient deathRecipient = verifyLinkToDeath(cb1);
-        verifyDaemonCommands("start-service", "register 2 a_name a_type 2201");
+        verify(mMockMDnsM, times(1)).registerService(
+                eq(2), eq("a_name"), eq("a_type"), eq(2201), any(), eq(0));
 
         client.unregisterService(listener1);
-        verifyDaemonCommand("stop-register 2");
+        waitForIdle();
+        verify(mMockMDnsM, times(1)).stopOperation(eq(2));
 
         verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
-        verifyDaemonCommand("stop-service");
-        reset(mDaemon);
+        reset(mMockMDnsM);
         deathRecipient.binderDied();
-        // Client disconnects, after CLEANUP_DELAY_MS, maybeStop the daemon.
-        verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
+        // Client disconnects, daemon should not be stopped after CLEANUP_DELAY_MS.
+        verify(mMockMDnsM, never()).unregisterEventListener(any());
+        verify(mMockMDnsM, never()).stopDaemon();
     }
 
     private void waitForIdle() {
@@ -267,11 +280,7 @@
     }
 
     NsdService makeService() {
-        DaemonConnectionSupplier supplier = (callback) -> {
-            mDaemonCallback = callback;
-            return mDaemon;
-        };
-        final NsdService service = new NsdService(mContext, mHandler, supplier, CLEANUP_DELAY_MS) {
+        final NsdService service = new NsdService(mContext, mHandler, CLEANUP_DELAY_MS) {
             @Override
             public INsdServiceConnector connect(INsdManagerCallback baseCb) {
                 // Wrap the callback in a transparent mock, to mock asBinder returning a
@@ -285,7 +294,6 @@
                 return super.connect(cb);
             }
         };
-        verify(mDaemon, never()).execute(any(String.class));
         return service;
     }
 
@@ -297,34 +305,15 @@
         return new NsdManager(mContext, service);
     }
 
-    void verifyDelayMaybeStopDaemon(long cleanupDelayMs) {
+    void verifyDelayMaybeStopDaemon(long cleanupDelayMs) throws Exception {
         waitForIdle();
         // Stop daemon shouldn't be called immediately.
-        verify(mDaemon, never()).maybeStop();
+        verify(mMockMDnsM, never()).unregisterEventListener(any());
+        verify(mMockMDnsM, never()).stopDaemon();
+
         // Clean up the daemon after CLEANUP_DELAY_MS.
-        verify(mDaemon, timeout(cleanupDelayMs + TIMEOUT_MS)).maybeStop();
-    }
-
-    void verifyDaemonCommands(String... wants) {
-        verifyDaemonCommand(String.join(" ", wants), wants.length);
-    }
-
-    void verifyDaemonCommand(String want) {
-        verifyDaemonCommand(want, 1);
-    }
-
-    void verifyDaemonCommand(String want, int n) {
-        waitForIdle();
-        final ArgumentCaptor<Object> argumentsCaptor = ArgumentCaptor.forClass(Object.class);
-        verify(mDaemon, times(n)).execute(argumentsCaptor.capture());
-        String got = "";
-        for (Object o : argumentsCaptor.getAllValues()) {
-            got += o + " ";
-        }
-        assertEquals(want, got.trim());
-        // rearm deamon for next command verification
-        reset(mDaemon);
-        doReturn(true).when(mDaemon).execute(any());
+        verify(mMockMDnsM, timeout(cleanupDelayMs + TIMEOUT_MS)).unregisterEventListener(any());
+        verify(mMockMDnsM, timeout(cleanupDelayMs + TIMEOUT_MS)).stopDaemon();
     }
 
     public static class TestHandler extends Handler {
diff --git a/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
index 6c8b545..c3d64cb 100644
--- a/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
@@ -17,11 +17,16 @@
 package com.android.server.connectivity;
 
 import static android.net.INetd.IF_STATE_UP;
+import static android.system.OsConstants.ETH_P_IP;
+import static android.system.OsConstants.ETH_P_IPV6;
 
 import static com.android.net.module.util.NetworkStackConstants.ETHER_MTU;
 import static com.android.server.connectivity.ClatCoordinator.CLAT_MAX_MTU;
+import static com.android.server.connectivity.ClatCoordinator.EGRESS;
+import static com.android.server.connectivity.ClatCoordinator.INGRESS;
 import static com.android.server.connectivity.ClatCoordinator.INIT_V4ADDR_PREFIX_LEN;
 import static com.android.server.connectivity.ClatCoordinator.INIT_V4ADDR_STRING;
+import static com.android.server.connectivity.ClatCoordinator.PRIO_CLAT;
 import static com.android.testutils.MiscAsserts.assertThrows;
 
 import static org.junit.Assert.assertEquals;
@@ -41,6 +46,11 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.bpf.ClatEgress4Key;
+import com.android.net.module.util.bpf.ClatEgress4Value;
+import com.android.net.module.util.bpf.ClatIngress6Key;
+import com.android.net.module.util.bpf.ClatIngress6Value;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 
@@ -98,8 +108,23 @@
     private static final ParcelFileDescriptor PACKET_SOCK_PFD = new ParcelFileDescriptor(
             new FileDescriptor());
 
+    private static final String EGRESS_PROG_PATH =
+            "/sys/fs/bpf/prog_clatd_schedcls_egress4_clat_rawip";
+    private static final String INGRESS_PROG_PATH =
+            "/sys/fs/bpf/prog_clatd_schedcls_ingress6_clat_ether";
+    private static final ClatEgress4Key EGRESS_KEY = new ClatEgress4Key(STACKED_IFINDEX,
+            INET4_LOCAL4);
+    private static final ClatEgress4Value EGRESS_VALUE = new ClatEgress4Value(BASE_IFINDEX,
+            INET6_LOCAL6, INET6_PFX96, (short) 1 /* oifIsEthernet, 1 = true */);
+    private static final ClatIngress6Key INGRESS_KEY = new ClatIngress6Key(BASE_IFINDEX,
+            INET6_PFX96, INET6_LOCAL6);
+    private static final ClatIngress6Value INGRESS_VALUE = new ClatIngress6Value(STACKED_IFINDEX,
+            INET4_LOCAL4);
+
     @Mock private INetd mNetd;
     @Spy private TestDependencies mDeps = new TestDependencies();
+    @Mock private IBpfMap<ClatIngress6Key, ClatIngress6Value> mIngressMap;
+    @Mock private IBpfMap<ClatEgress4Key, ClatEgress4Value> mEgressMap;
 
     /**
       * The dependency injection class is used to mock the JNI functions and system functions
@@ -298,6 +323,49 @@
                 fail("unsupported arg: " + cookie);
             }
         }
+
+        /** Get ingress6 BPF map. */
+        @Override
+        public IBpfMap<ClatIngress6Key, ClatIngress6Value> getBpfIngress6Map() {
+            return mIngressMap;
+        }
+
+        /** Get egress4 BPF map. */
+        @Override
+        public IBpfMap<ClatEgress4Key, ClatEgress4Value> getBpfEgress4Map() {
+            return mEgressMap;
+        }
+
+        /** Checks if the network interface uses an ethernet L2 header. */
+        public boolean isEthernet(String iface) throws IOException {
+            if (BASE_IFACE.equals(iface)) return true;
+
+            fail("unsupported arg: " + iface);
+            return false;
+        }
+
+        /** Add a clsact qdisc. */
+        @Override
+        public void tcQdiscAddDevClsact(int ifIndex) throws IOException {
+            // no-op
+            return;
+        }
+
+        /** Attach a tc bpf filter. */
+        @Override
+        public void tcFilterAddDevBpf(int ifIndex, boolean ingress, short prio, short proto,
+                String bpfProgPath) throws IOException {
+            // no-op
+            return;
+        }
+
+        /** Delete a tc filter. */
+        @Override
+        public void tcFilterDelDev(int ifIndex, boolean ingress, short prio, short proto)
+                throws IOException {
+            // no-op
+            return;
+        }
     };
 
     @NonNull
@@ -322,8 +390,8 @@
     @Test
     public void testStartStopClatd() throws Exception {
         final ClatCoordinator coordinator = makeClatCoordinator();
-        final InOrder inOrder = inOrder(mNetd, mDeps);
-        clearInvocations(mNetd, mDeps);
+        final InOrder inOrder = inOrder(mNetd, mDeps, mIngressMap, mEgressMap);
+        clearInvocations(mNetd, mDeps, mIngressMap, mEgressMap);
 
         // [1] Start clatd.
         final String addr6For464xlat = coordinator.clatStart(BASE_IFACE, NETID, NAT64_IP_PREFIX);
@@ -379,6 +447,13 @@
                 argThat(fd -> Objects.equals(RAW_SOCK_PFD.getFileDescriptor(), fd)),
                 eq(BASE_IFACE), eq(NAT64_PREFIX_STRING),
                 eq(XLAT_LOCAL_IPV4ADDR_STRING), eq(XLAT_LOCAL_IPV6ADDR_STRING));
+        inOrder.verify(mEgressMap).insertEntry(eq(EGRESS_KEY), eq(EGRESS_VALUE));
+        inOrder.verify(mIngressMap).insertEntry(eq(INGRESS_KEY), eq(INGRESS_VALUE));
+        inOrder.verify(mDeps).tcQdiscAddDevClsact(eq(STACKED_IFINDEX));
+        inOrder.verify(mDeps).tcFilterAddDevBpf(eq(STACKED_IFINDEX), eq(EGRESS),
+                eq((short) PRIO_CLAT), eq((short) ETH_P_IP), eq(EGRESS_PROG_PATH));
+        inOrder.verify(mDeps).tcFilterAddDevBpf(eq(BASE_IFINDEX), eq(INGRESS),
+                eq((short) PRIO_CLAT), eq((short) ETH_P_IPV6), eq(INGRESS_PROG_PATH));
         inOrder.verifyNoMoreInteractions();
 
         // [2] Start clatd again failed.
@@ -388,6 +463,12 @@
 
         // [3] Expect clatd to stop successfully.
         coordinator.clatStop();
+        inOrder.verify(mDeps).tcFilterDelDev(eq(BASE_IFINDEX), eq(INGRESS),
+                eq((short) PRIO_CLAT), eq((short) ETH_P_IPV6));
+        inOrder.verify(mDeps).tcFilterDelDev(eq(STACKED_IFINDEX), eq(EGRESS),
+                eq((short) PRIO_CLAT), eq((short) ETH_P_IP));
+        inOrder.verify(mEgressMap).deleteEntry(eq(EGRESS_KEY));
+        inOrder.verify(mIngressMap).deleteEntry(eq(INGRESS_KEY));
         inOrder.verify(mDeps).stopClatd(eq(BASE_IFACE), eq(NAT64_PREFIX_STRING),
                 eq(XLAT_LOCAL_IPV4ADDR_STRING), eq(XLAT_LOCAL_IPV6ADDR_STRING), eq(CLATD_PID));
         inOrder.verify(mDeps).untagSocket(eq(RAW_SOCK_COOKIE));
diff --git a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
index 6b379e8..fb821c3 100644
--- a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
@@ -117,23 +117,32 @@
 public class PermissionMonitorTest {
     private static final int MOCK_USER_ID1 = 0;
     private static final int MOCK_USER_ID2 = 1;
+    private static final int MOCK_USER_ID3 = 2;
     private static final UserHandle MOCK_USER1 = UserHandle.of(MOCK_USER_ID1);
     private static final UserHandle MOCK_USER2 = UserHandle.of(MOCK_USER_ID2);
+    private static final UserHandle MOCK_USER3 = UserHandle.of(MOCK_USER_ID3);
     private static final int MOCK_APPID1 = 10001;
     private static final int MOCK_APPID2 = 10086;
+    private static final int MOCK_APPID3 = 10110;
     private static final int SYSTEM_APPID1 = 1100;
     private static final int SYSTEM_APPID2 = 1108;
     private static final int VPN_APPID = 10002;
     private static final int MOCK_UID11 = MOCK_USER1.getUid(MOCK_APPID1);
     private static final int MOCK_UID12 = MOCK_USER1.getUid(MOCK_APPID2);
+    private static final int MOCK_UID13 = MOCK_USER1.getUid(MOCK_APPID3);
     private static final int SYSTEM_APP_UID11 = MOCK_USER1.getUid(SYSTEM_APPID1);
     private static final int VPN_UID = MOCK_USER1.getUid(VPN_APPID);
     private static final int MOCK_UID21 = MOCK_USER2.getUid(MOCK_APPID1);
     private static final int MOCK_UID22 = MOCK_USER2.getUid(MOCK_APPID2);
+    private static final int MOCK_UID23 = MOCK_USER2.getUid(MOCK_APPID3);
     private static final int SYSTEM_APP_UID21 = MOCK_USER2.getUid(SYSTEM_APPID1);
+    private static final int MOCK_UID31 = MOCK_USER3.getUid(MOCK_APPID1);
+    private static final int MOCK_UID32 = MOCK_USER3.getUid(MOCK_APPID2);
+    private static final int MOCK_UID33 = MOCK_USER3.getUid(MOCK_APPID3);
     private static final String REAL_SYSTEM_PACKAGE_NAME = "android";
     private static final String MOCK_PACKAGE1 = "appName1";
     private static final String MOCK_PACKAGE2 = "appName2";
+    private static final String MOCK_PACKAGE3 = "appName3";
     private static final String SYSTEM_PACKAGE1 = "sysName1";
     private static final String SYSTEM_PACKAGE2 = "sysName2";
     private static final String PARTITION_SYSTEM = "system";
@@ -191,6 +200,7 @@
         mBpfMapMonitor = new BpfMapMonitor(mBpfNetMaps);
 
         doReturn(List.of()).when(mPackageManager).getInstalledPackagesAsUser(anyInt(), anyInt());
+        mPermissionMonitor.onUserAdded(MOCK_USER1);
     }
 
     private boolean hasRestrictedNetworkPermission(String partition, int targetSdkVersion,
@@ -283,6 +293,18 @@
         mPermissionMonitor.onPackageAdded(packageName, uid);
     }
 
+    private void removePackage(String packageName, int uid) {
+        final String[] oldPackages = mPackageManager.getPackagesForUid(uid);
+        // If the package isn't existed, no need to remove it.
+        if (!CollectionUtils.contains(oldPackages, packageName)) return;
+
+        // Remove the package if this uid is shared with other packages.
+        final String[] newPackages = Arrays.stream(oldPackages).filter(e -> !e.equals(packageName))
+                .toArray(String[]::new);
+        doReturn(newPackages).when(mPackageManager).getPackagesForUid(eq(uid));
+        mPermissionMonitor.onPackageRemoved(packageName, uid);
+    }
+
     @Test
     public void testHasPermission() {
         PackageInfo app = systemPackageInfoWithPermissions();
@@ -791,6 +813,7 @@
                 buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
                 .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
         buildAndMockPackageInfoWithPermissions(MOCK_PACKAGE1, MOCK_UID11);
+        doReturn(List.of(MOCK_USER1, MOCK_USER2)).when(mUserManager).getUserHandles(eq(true));
 
         mPermissionMonitor.startMonitoring();
         final Set<UidRange> vpnRange = Set.of(UidRange.createForUser(MOCK_USER1),
@@ -881,7 +904,7 @@
         addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET, UPDATE_DEVICE_STATS);
         mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1);
 
-        // Install another package with the same uid and no permissions should not cause the app id
+        // Install another package with the same uid and no permissions should not cause the appId
         // to lose permissions.
         addPackage(MOCK_PACKAGE2, MOCK_UID11);
         mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1);
@@ -1249,4 +1272,211 @@
         assertTrue(isHigherNetworkPermission(PERMISSION_SYSTEM, PERMISSION_NETWORK));
         assertFalse(isHigherNetworkPermission(PERMISSION_SYSTEM, PERMISSION_SYSTEM));
     }
+
+    private void prepareMultiUserPackages() {
+        // MOCK_USER1 has installed 3 packages
+        // mockApp1 has no permission and share MOCK_APPID1.
+        // mockApp2 has INTERNET permission and share MOCK_APPID2.
+        // mockApp3 has UPDATE_DEVICE_STATS permission and share MOCK_APPID3.
+        final List<PackageInfo> pkgs1 = List.of(
+                buildPackageInfo("mockApp1", MOCK_UID11),
+                buildPackageInfo("mockApp2", MOCK_UID12, INTERNET),
+                buildPackageInfo("mockApp3", MOCK_UID13, UPDATE_DEVICE_STATS));
+
+        // MOCK_USER2 has installed 2 packages
+        // mockApp4 has UPDATE_DEVICE_STATS permission and share MOCK_APPID1.
+        // mockApp5 has INTERNET permission and share MOCK_APPID2.
+        final List<PackageInfo> pkgs2 = List.of(
+                buildPackageInfo("mockApp4", MOCK_UID21, UPDATE_DEVICE_STATS),
+                buildPackageInfo("mockApp5", MOCK_UID23, INTERNET));
+
+        // MOCK_USER3 has installed 1 packages
+        // mockApp6 has UPDATE_DEVICE_STATS permission and share MOCK_APPID2.
+        final List<PackageInfo> pkgs3 = List.of(
+                buildPackageInfo("mockApp6", MOCK_UID32, UPDATE_DEVICE_STATS));
+
+        doReturn(pkgs1).when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS),
+                eq(MOCK_USER_ID1));
+        doReturn(pkgs2).when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS),
+                eq(MOCK_USER_ID2));
+        doReturn(pkgs3).when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS),
+                eq(MOCK_USER_ID3));
+    }
+
+    private void addUserAndVerifyAppIdsPermissions(UserHandle user, int appId1Perm,
+            int appId2Perm, int appId3Perm) {
+        mPermissionMonitor.onUserAdded(user);
+        mBpfMapMonitor.expectTrafficPerm(appId1Perm, MOCK_APPID1);
+        mBpfMapMonitor.expectTrafficPerm(appId2Perm, MOCK_APPID2);
+        mBpfMapMonitor.expectTrafficPerm(appId3Perm, MOCK_APPID3);
+    }
+
+    private void removeUserAndVerifyAppIdsPermissions(UserHandle user, int appId1Perm,
+            int appId2Perm, int appId3Perm) {
+        mPermissionMonitor.onUserRemoved(user);
+        mBpfMapMonitor.expectTrafficPerm(appId1Perm, MOCK_APPID1);
+        mBpfMapMonitor.expectTrafficPerm(appId2Perm, MOCK_APPID2);
+        mBpfMapMonitor.expectTrafficPerm(appId3Perm, MOCK_APPID3);
+    }
+
+    @Test
+    public void testAppIdsTrafficPermission_UserAddedRemoved() {
+        prepareMultiUserPackages();
+
+        // Add MOCK_USER1 and verify the permissions with each appIds.
+        addUserAndVerifyAppIdsPermissions(MOCK_USER1, PERMISSION_NONE, PERMISSION_INTERNET,
+                PERMISSION_UPDATE_DEVICE_STATS);
+
+        // Add MOCK_USER2 and verify the permissions upgrade on MOCK_APPID1 & MOCK_APPID3.
+        addUserAndVerifyAppIdsPermissions(MOCK_USER2, PERMISSION_UPDATE_DEVICE_STATS,
+                PERMISSION_INTERNET, PERMISSION_TRAFFIC_ALL);
+
+        // Add MOCK_USER3 and verify the permissions upgrade on MOCK_APPID2.
+        addUserAndVerifyAppIdsPermissions(MOCK_USER3, PERMISSION_UPDATE_DEVICE_STATS,
+                PERMISSION_TRAFFIC_ALL, PERMISSION_TRAFFIC_ALL);
+
+        // Remove MOCK_USER2 and verify the permissions downgrade on MOCK_APPID1 & MOCK_APPID3.
+        removeUserAndVerifyAppIdsPermissions(MOCK_USER2, PERMISSION_NONE, PERMISSION_TRAFFIC_ALL,
+                PERMISSION_UPDATE_DEVICE_STATS);
+
+        // Remove MOCK_USER1 and verify the permissions downgrade on all appIds.
+        removeUserAndVerifyAppIdsPermissions(MOCK_USER1, PERMISSION_UNINSTALLED,
+                PERMISSION_UPDATE_DEVICE_STATS, PERMISSION_UNINSTALLED);
+
+        // Add MOCK_USER2 back and verify the permissions upgrade on MOCK_APPID1 & MOCK_APPID3.
+        addUserAndVerifyAppIdsPermissions(MOCK_USER2, PERMISSION_UPDATE_DEVICE_STATS,
+                PERMISSION_UPDATE_DEVICE_STATS, PERMISSION_INTERNET);
+
+        // Remove MOCK_USER3 and verify the permissions downgrade on MOCK_APPID2.
+        removeUserAndVerifyAppIdsPermissions(MOCK_USER3, PERMISSION_UPDATE_DEVICE_STATS,
+                PERMISSION_UNINSTALLED, PERMISSION_INTERNET);
+    }
+
+    @Test
+    public void testAppIdsTrafficPermission_Multiuser_PackageAdded() throws Exception {
+        // Add two users with empty package list.
+        mPermissionMonitor.onUserAdded(MOCK_USER1);
+        mPermissionMonitor.onUserAdded(MOCK_USER2);
+
+        final int[] netdPermissions = {PERMISSION_NONE, PERMISSION_INTERNET,
+                PERMISSION_UPDATE_DEVICE_STATS, PERMISSION_TRAFFIC_ALL};
+        final String[][] grantPermissions = {new String[]{}, new String[]{INTERNET},
+                new String[]{UPDATE_DEVICE_STATS}, new String[]{INTERNET, UPDATE_DEVICE_STATS}};
+
+        // Verify that the permission combination is expected when same appId package is installed
+        // on another user. List the expected permissions below.
+        // NONE                + NONE                = NONE
+        // NONE                + INTERNET            = INTERNET
+        // NONE                + UPDATE_DEVICE_STATS = UPDATE_DEVICE_STATS
+        // NONE                + ALL                 = ALL
+        // INTERNET            + NONE                = INTERNET
+        // INTERNET            + INTERNET            = INTERNET
+        // INTERNET            + UPDATE_DEVICE_STATS = ALL
+        // INTERNET            + ALL                 = ALL
+        // UPDATE_DEVICE_STATS + NONE                = UPDATE_DEVICE_STATS
+        // UPDATE_DEVICE_STATS + INTERNET            = ALL
+        // UPDATE_DEVICE_STATS + UPDATE_DEVICE_STATS = UPDATE_DEVICE_STATS
+        // UPDATE_DEVICE_STATS + ALL                 = ALL
+        // ALL                 + NONE                = ALL
+        // ALL                 + INTERNET            = ALL
+        // ALL                 + UPDATE_DEVICE_STATS = ALL
+        // ALL                 + ALL                 = ALL
+        for (int i = 0, num = 0; i < netdPermissions.length; i++) {
+            final int current = netdPermissions[i];
+            final String[] user1Perm = grantPermissions[i];
+            for (int j = 0; j < netdPermissions.length; j++) {
+                final int appId = MOCK_APPID1 + num;
+                final int added = netdPermissions[j];
+                final String[] user2Perm = grantPermissions[j];
+                // Add package on MOCK_USER1 and verify the permission is same as package granted.
+                addPackage(MOCK_PACKAGE1, MOCK_USER1.getUid(appId), user1Perm);
+                mBpfMapMonitor.expectTrafficPerm(current, appId);
+
+                // Add package which share the same appId on MOCK_USER2, and verify the permission
+                // has combined.
+                addPackage(MOCK_PACKAGE2, MOCK_USER2.getUid(appId), user2Perm);
+                mBpfMapMonitor.expectTrafficPerm((current | added), appId);
+                num++;
+            }
+        }
+    }
+
+    private void verifyAppIdPermissionsAfterPackageRemoved(int appId, int expectedPerm,
+            String[] user1Perm, String[] user2Perm) throws Exception {
+        // Add package on MOCK_USER1 and verify the permission is same as package granted.
+        addPackage(MOCK_PACKAGE1, MOCK_USER1.getUid(appId), user1Perm);
+        mBpfMapMonitor.expectTrafficPerm(expectedPerm, appId);
+
+        // Add two packages which share the same appId and don't declare permission on
+        // MOCK_USER2. Verify the permission has no change.
+        addPackage(MOCK_PACKAGE2, MOCK_USER2.getUid(appId));
+        addPackage(MOCK_PACKAGE3, MOCK_USER2.getUid(appId), user2Perm);
+        mBpfMapMonitor.expectTrafficPerm(expectedPerm, appId);
+
+        // Remove one packages from MOCK_USER2. Verify the permission has no change too.
+        removePackage(MOCK_PACKAGE2, MOCK_USER2.getUid(appId));
+        mBpfMapMonitor.expectTrafficPerm(expectedPerm, appId);
+
+        // Remove last packages from MOCK_USER2. Verify the permission has still no change.
+        removePackage(MOCK_PACKAGE3, MOCK_USER2.getUid(appId));
+        mBpfMapMonitor.expectTrafficPerm(expectedPerm, appId);
+    }
+
+    @Test
+    public void testAppIdsTrafficPermission_Multiuser_PackageRemoved() throws Exception {
+        // Add two users with empty package list.
+        mPermissionMonitor.onUserAdded(MOCK_USER1);
+        mPermissionMonitor.onUserAdded(MOCK_USER2);
+
+        int appId = MOCK_APPID1;
+        // Verify that the permission combination is expected when same appId package is removed on
+        // another user. List the expected permissions below.
+        /***** NONE *****/
+        // NONE + NONE = NONE
+        verifyAppIdPermissionsAfterPackageRemoved(
+                appId++, PERMISSION_NONE, new String[]{}, new String[]{});
+
+        /***** INTERNET *****/
+        // INTERNET + NONE = INTERNET
+        verifyAppIdPermissionsAfterPackageRemoved(
+                appId++, PERMISSION_INTERNET, new String[]{INTERNET}, new String[]{});
+
+        // INTERNET + INTERNET = INTERNET
+        verifyAppIdPermissionsAfterPackageRemoved(
+                appId++, PERMISSION_INTERNET, new String[]{INTERNET}, new String[]{INTERNET});
+
+        /***** UPDATE_DEVICE_STATS *****/
+        // UPDATE_DEVICE_STATS + NONE = UPDATE_DEVICE_STATS
+        verifyAppIdPermissionsAfterPackageRemoved(appId++, PERMISSION_UPDATE_DEVICE_STATS,
+                new String[]{UPDATE_DEVICE_STATS}, new String[]{});
+
+        // UPDATE_DEVICE_STATS + UPDATE_DEVICE_STATS = UPDATE_DEVICE_STATS
+        verifyAppIdPermissionsAfterPackageRemoved(appId++, PERMISSION_UPDATE_DEVICE_STATS,
+                new String[]{UPDATE_DEVICE_STATS}, new String[]{UPDATE_DEVICE_STATS});
+
+        /***** ALL *****/
+        // ALL + NONE = ALL
+        verifyAppIdPermissionsAfterPackageRemoved(appId++, PERMISSION_TRAFFIC_ALL,
+                new String[]{INTERNET, UPDATE_DEVICE_STATS}, new String[]{});
+
+        // ALL + INTERNET = ALL
+        verifyAppIdPermissionsAfterPackageRemoved(appId++, PERMISSION_TRAFFIC_ALL,
+                new String[]{INTERNET, UPDATE_DEVICE_STATS}, new String[]{INTERNET});
+
+        // ALL + UPDATE_DEVICE_STATS = ALL
+        verifyAppIdPermissionsAfterPackageRemoved(appId++, PERMISSION_TRAFFIC_ALL,
+                new String[]{INTERNET, UPDATE_DEVICE_STATS}, new String[]{UPDATE_DEVICE_STATS});
+
+        // ALL + ALL = ALL
+        verifyAppIdPermissionsAfterPackageRemoved(appId++, PERMISSION_TRAFFIC_ALL,
+                new String[]{INTERNET, UPDATE_DEVICE_STATS},
+                new String[]{INTERNET, UPDATE_DEVICE_STATS});
+
+        /***** UNINSTALL *****/
+        // UNINSTALL + UNINSTALL = UNINSTALL
+        verifyAppIdPermissionsAfterPackageRemoved(
+                appId, PERMISSION_NONE, new String[]{}, new String[]{});
+        removePackage(MOCK_PACKAGE1, MOCK_USER1.getUid(appId));
+        mBpfMapMonitor.expectTrafficPerm(PERMISSION_UNINSTALLED, appId);
+    }
 }
diff --git a/tests/ethernet/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
similarity index 97%
rename from tests/ethernet/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
rename to tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
index 4d3e4d3..dfb4fcc 100644
--- a/tests/ethernet/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
+++ b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
@@ -20,9 +20,9 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
@@ -41,8 +41,8 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.net.ConnectivityManager;
-import android.net.EthernetNetworkSpecifier;
 import android.net.EthernetNetworkManagementException;
+import android.net.EthernetNetworkSpecifier;
 import android.net.INetworkInterfaceOutcomeReceiver;
 import android.net.IpConfiguration;
 import android.net.LinkAddress;
@@ -59,14 +59,11 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.test.TestLooper;
-import android.util.Pair;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.connectivity.resources.R;
 import com.android.net.module.util.InterfaceParams;
-
 import com.android.testutils.DevSdkIgnoreRule;
 
 import org.junit.After;
@@ -79,6 +76,7 @@
 
 import java.util.Objects;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 
 @RunWith(AndroidJUnit4.class)
@@ -362,7 +360,7 @@
 
         assertFalse(ret);
         verifyNoStopOrStart();
-        listener.expectOnErrorWithMessage("can't be updated as it is not available");
+        listener.expectOnError();
     }
 
     @Test
@@ -376,7 +374,7 @@
 
         assertFalse(ret);
         verifyNoStopOrStart();
-        listener.expectOnErrorWithMessage("No changes");
+        listener.expectOnError();
     }
 
     @Test
@@ -626,8 +624,6 @@
     private static final class TestNetworkManagementListener
             implements INetworkInterfaceOutcomeReceiver {
         private final CompletableFuture<String> mResult = new CompletableFuture<>();
-        private final CompletableFuture<EthernetNetworkManagementException> mError =
-                new CompletableFuture<>();
 
         @Override
         public void onResult(@NonNull String iface) {
@@ -636,19 +632,21 @@
 
         @Override
         public void onError(@NonNull EthernetNetworkManagementException exception) {
-            mError.complete(exception);
+            mResult.completeExceptionally(exception);
         }
 
         String expectOnResult() throws Exception {
             return mResult.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
         }
 
-        EthernetNetworkManagementException expectOnError() throws Exception {
-            return mError.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
-        }
-
-        void expectOnErrorWithMessage(String msg) throws Exception {
-            assertTrue(expectOnError().getMessage().contains(msg));
+        void expectOnError() throws Exception {
+            assertThrows(EthernetNetworkManagementException.class, () -> {
+                try {
+                    mResult.get();
+                } catch (ExecutionException e) {
+                    throw e.getCause();
+                }
+            });
         }
 
         @Override
@@ -723,7 +721,7 @@
         mNetFactory.updateInterface(iface, ipConfiguration, capabilities, failedListener);
         interruptingRunnable.run();
 
-        failedListener.expectOnErrorWithMessage("aborted");
+        failedListener.expectOnError();
     }
 
     @Test
@@ -754,7 +752,7 @@
         mNetFactory.updateInterface(TEST_IFACE, ipConfiguration, capabilities, listener);
 
         verifyNoStopOrStart();
-        listener.expectOnErrorWithMessage("can't be updated as it is not available");
+        listener.expectOnError();
     }
 
     @Test
diff --git a/tests/ethernet/java/com/android/server/ethernet/EthernetServiceImplTest.java b/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java
similarity index 100%
rename from tests/ethernet/java/com/android/server/ethernet/EthernetServiceImplTest.java
rename to tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java
diff --git a/tests/ethernet/java/com/android/server/ethernet/EthernetTrackerTest.java b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
similarity index 100%
rename from tests/ethernet/java/com/android/server/ethernet/EthernetTrackerTest.java
rename to tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java