Merge changes from topic "access_clat_bpf_map_from_clatcoordinator"

* changes:
  ClatCoordinator: use Java class BpfMap to remove clat rule
  ClatCoordinator: use Java class BpfMap to add clat rule
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/Android.bp b/Tethering/Android.bp
index 41a0651..29f6e12 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -47,6 +47,7 @@
         "net-utils-device-common-bpf",
         "net-utils-device-common-netlink",
         "netd-client",
+        "tetheringstatsprotos",
     ],
     libs: [
         "framework-connectivity",
@@ -211,3 +212,14 @@
     bootclasspath_fragments: ["com.android.tethering-bootclasspath-fragment"],
     systemserverclasspath_fragments: ["com.android.tethering-systemserverclasspath-fragment"],
 }
+
+java_library_static {
+    name: "tetheringstatsprotos",
+    proto: {type: "lite"},
+    srcs: [
+        "src/com/android/networkstack/tethering/metrics/stats.proto",
+    ],
+    static_libs: ["tetheringprotos"],
+    apex_available: ["com.android.tethering"],
+    min_sdk_version: "30",
+}
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index dd04d6c..ac777d7 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",
     ],
@@ -131,15 +132,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/jarjar-rules.txt b/Tethering/jarjar-rules.txt
index 5de4b97..40eed3f 100644
--- a/Tethering/jarjar-rules.txt
+++ b/Tethering/jarjar-rules.txt
@@ -12,3 +12,5 @@
 
 # Classes from net-utils-device-common
 rule com.android.net.module.util.Struct* com.android.networkstack.tethering.util.Struct@1
+
+rule com.google.protobuf.** com.android.networkstack.tethering.protobuf@1
\ No newline at end of file
diff --git a/Tethering/proguard.flags b/Tethering/proguard.flags
index 6735317..7b5ae0d 100644
--- a/Tethering/proguard.flags
+++ b/Tethering/proguard.flags
@@ -19,3 +19,9 @@
 -keepclassmembers class android.net.ip.IpServer {
     static final int CMD_*;
 }
+
+# The lite proto runtime uses reflection to access fields based on the names in
+# the schema, keep all the fields.
+-keepclassmembers class * extends com.android.networkstack.tethering.protobuf.MessageLite {
+    <fields>;
+}
\ No newline at end of file
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/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/metrics/stats.proto b/Tethering/src/com/android/networkstack/tethering/metrics/stats.proto
new file mode 100644
index 0000000..46a47af
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/metrics/stats.proto
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+syntax = "proto2";
+option java_multiple_files = true;
+
+package com.android.networkstack.tethering.metrics;
+
+import "frameworks/proto_logging/stats/enums/stats/connectivity/tethering.proto";
+
+/**
+ * Logs Tethering events
+ */
+message NetworkTetheringReported {
+   optional .android.stats.connectivity.ErrorCode error_code = 1;
+   optional .android.stats.connectivity.DownstreamType downstream_type = 2;
+   optional .android.stats.connectivity.UpstreamType upstream_type = 3;
+   optional .android.stats.connectivity.UserType user_type = 4;
+}
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/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..43f1eaa 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -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/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/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/clatd.c b/bpf_progs/clatd.c
index dc646c3..8971f6b 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_readable(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_readable(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/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/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/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..3703df8 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -86,6 +86,7 @@
         "net-utils-device-common",
     ],
     static_libs: [
+        "mdns_aidl_interface-lateststable-java",
         "modules-utils-backgroundthread",
         "modules-utils-build",
         "modules-utils-preconditions",
diff --git a/service-t/Android.bp b/service-t/Android.bp
index 50dd995..1b9f2ec 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -22,6 +22,17 @@
 // Include build rules from Sources.bp
 build = ["Sources.bp"]
 
+filegroup {
+    name: "service-connectivity-tiramisu-sources",
+    srcs: [
+        "src/**/*.java",
+    ],
+    visibility: ["//visibility:private"],
+}
+// The above filegroup can be used to specify different sources depending
+// on the branch, while minimizing merge conflicts in the rest of the
+// build rules.
+
 // This builds T+ services depending on framework-connectivity-t
 // hidden symbols separately from the S+ services, to ensure that S+
 // services cannot accidentally depend on T+ hidden symbols from
@@ -32,9 +43,7 @@
     // TODO(b/210962470): Bump this to at least S, and then T.
     min_sdk_version: "30",
     srcs: [
-        "src/**/*.java",
-        ":ethernet-service-updatable-sources",
-        ":services.connectivity-tiramisu-updatable-sources",
+        ":service-connectivity-tiramisu-sources",
     ],
     libs: [
         "framework-annotations-lib",
diff --git a/service-t/Sources.bp b/service-t/Sources.bp
index 04866fb..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,93 +38,3 @@
     ],
 }
 
-// Nsd related libraries.
-
-filegroup {
-    name: "services.connectivity-nsd-sources",
-    srcs: [
-        "src/com/android/server/INativeDaemon*.java",
-        "src/com/android/server/NativeDaemon*.java",
-        "src/com/android/server/Nsd*.java",
-    ],
-    path: "src",
-    visibility: [
-        "//visibility:private",
-    ],
-}
-
-// IpSec related libraries.
-
-filegroup {
-    name: "services.connectivity-ipsec-sources",
-    srcs: [
-        "src/com/android/server/IpSecService.java",
-    ],
-    path: "src",
-    visibility: [
-        "//visibility:private",
-    ],
-}
-
-// Ethernet related libraries.
-
-filegroup {
-    name: "services.connectivity-ethernet-sources",
-    srcs: [
-        "src/com/android/server/net/DelayedDiskWrite.java",
-        "src/com/android/server/net/IpConfigStore.java",
-    ],
-    path: "src",
-    visibility: [
-        "//frameworks/opt/net/ethernet/tests",
-    ],
-}
-
-// Connectivity-T common libraries.
-
-// TODO: remove this empty filegroup.
-filegroup {
-    name: "services.connectivity-tiramisu-sources",
-    srcs: [],
-    path: "src",
-    visibility: ["//frameworks/base/services/core"],
-}
-
-filegroup {
-    name: "services.connectivity-tiramisu-updatable-sources",
-    srcs: [
-        ":services.connectivity-ethernet-sources",
-        ":services.connectivity-ipsec-sources",
-        ":services.connectivity-netstats-sources",
-        ":services.connectivity-nsd-sources",
-    ],
-    path: "src",
-    visibility: [
-        "//packages/modules/Connectivity:__subpackages__",
-    ],
-}
-
-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/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/EthernetConfigStore.java b/service-t/src/com/android/server/ethernet/EthernetConfigStore.java
new file mode 100644
index 0000000..6b623f4
--- /dev/null
+++ b/service-t/src/com/android/server/ethernet/EthernetConfigStore.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2014 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.ethernet;
+
+import android.annotation.Nullable;
+import android.net.IpConfiguration;
+import android.os.Environment;
+import android.util.ArrayMap;
+
+import com.android.server.net.IpConfigStore;
+
+
+/**
+ * This class provides an API to store and manage Ethernet network configuration.
+ */
+public class EthernetConfigStore {
+    private static final String ipConfigFile = Environment.getDataDirectory() +
+            "/misc/ethernet/ipconfig.txt";
+
+    private IpConfigStore mStore = new IpConfigStore();
+    private ArrayMap<String, IpConfiguration> mIpConfigurations;
+    private IpConfiguration mIpConfigurationForDefaultInterface;
+    private final Object mSync = new Object();
+
+    public EthernetConfigStore() {
+        mIpConfigurations = new ArrayMap<>(0);
+    }
+
+    public void read() {
+        synchronized (mSync) {
+            ArrayMap<String, IpConfiguration> configs =
+                    IpConfigStore.readIpConfigurations(ipConfigFile);
+
+            // This configuration may exist in old file versions when there was only a single active
+            // Ethernet interface.
+            if (configs.containsKey("0")) {
+                mIpConfigurationForDefaultInterface = configs.remove("0");
+            }
+
+            mIpConfigurations = configs;
+        }
+    }
+
+    public void write(String iface, IpConfiguration config) {
+        boolean modified;
+
+        synchronized (mSync) {
+            if (config == null) {
+                modified = mIpConfigurations.remove(iface) != null;
+            } else {
+                IpConfiguration oldConfig = mIpConfigurations.put(iface, config);
+                modified = !config.equals(oldConfig);
+            }
+
+            if (modified) {
+                mStore.writeIpConfigurations(ipConfigFile, mIpConfigurations);
+            }
+        }
+    }
+
+    public ArrayMap<String, IpConfiguration> getIpConfigurations() {
+        synchronized (mSync) {
+            return new ArrayMap<>(mIpConfigurations);
+        }
+    }
+
+    @Nullable
+    public IpConfiguration getIpConfigurationForDefaultInterface() {
+        synchronized (mSync) {
+            return mIpConfigurationForDefaultInterface == null
+                    ? null : new IpConfiguration(mIpConfigurationForDefaultInterface);
+        }
+    }
+}
diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkAgent.java b/service-t/src/com/android/server/ethernet/EthernetNetworkAgent.java
new file mode 100644
index 0000000..57fbce7
--- /dev/null
+++ b/service-t/src/com/android/server/ethernet/EthernetNetworkAgent.java
@@ -0,0 +1,65 @@
+/*
+ * 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.ethernet;
+
+import android.content.Context;
+import android.net.LinkProperties;
+import android.net.NetworkAgent;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkProvider;
+import android.net.NetworkScore;
+import android.os.Looper;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+public class EthernetNetworkAgent extends NetworkAgent {
+
+    private static final String TAG = "EthernetNetworkAgent";
+
+    public interface Callbacks {
+        void onNetworkUnwanted();
+    }
+
+    private final Callbacks mCallbacks;
+
+    EthernetNetworkAgent(
+            @NonNull Context context,
+            @NonNull Looper looper,
+            @NonNull NetworkCapabilities nc,
+            @NonNull LinkProperties lp,
+            @NonNull NetworkAgentConfig config,
+            @Nullable NetworkProvider provider,
+            @NonNull Callbacks cb) {
+        super(context, looper, TAG, nc, lp, new NetworkScore.Builder().build(), config, provider);
+        mCallbacks = cb;
+    }
+
+    @Override
+    public void onNetworkUnwanted() {
+        mCallbacks.onNetworkUnwanted();
+    }
+
+    // sendLinkProperties is final in NetworkAgent, so it cannot be mocked.
+    public void sendLinkPropertiesImpl(LinkProperties lp) {
+        sendLinkProperties(lp);
+    }
+
+    public Callbacks getCallbacks() {
+        return mCallbacks;
+    }
+}
diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
new file mode 100644
index 0000000..eb22f78
--- /dev/null
+++ b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
@@ -0,0 +1,784 @@
+/*
+ * Copyright (C) 2014 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.ethernet;
+
+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.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.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkFactory;
+import android.net.NetworkProvider;
+import android.net.NetworkRequest;
+import android.net.NetworkSpecifier;
+import android.net.ip.IIpClient;
+import android.net.ip.IpClientCallbacks;
+import android.net.ip.IpClientManager;
+import android.net.ip.IpClientUtil;
+import android.net.shared.ProvisioningConfiguration;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.AndroidRuntimeException;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.connectivity.resources.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.net.module.util.InterfaceParams;
+
+import java.io.FileDescriptor;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * {@link NetworkFactory} that represents Ethernet networks.
+ *
+ * This class reports a static network score of 70 when it is tracking an interface and that
+ * interface's link is up, and a score of 0 otherwise.
+ */
+public class EthernetNetworkFactory extends NetworkFactory {
+    private final static String TAG = EthernetNetworkFactory.class.getSimpleName();
+    final static boolean DBG = true;
+
+    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<>();
+    private final Handler mHandler;
+    private final Context mContext;
+    final Dependencies mDeps;
+
+    public static class Dependencies {
+        public void makeIpClient(Context context, String iface, IpClientCallbacks callbacks) {
+            IpClientUtil.makeIpClient(context, iface, callbacks);
+        }
+
+        public IpClientManager makeIpClientManager(@NonNull final IIpClient ipClient) {
+            return new IpClientManager(ipClient, TAG);
+        }
+
+        public EthernetNetworkAgent makeEthernetNetworkAgent(Context context, Looper looper,
+                NetworkCapabilities nc, LinkProperties lp, NetworkAgentConfig config,
+                NetworkProvider provider, EthernetNetworkAgent.Callbacks cb) {
+            return new EthernetNetworkAgent(context, looper, nc, lp, config, provider, cb);
+        }
+
+        public InterfaceParams getNetworkInterfaceByName(String name) {
+            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;
+        }
+    }
+
+    public static class ConfigurationException extends AndroidRuntimeException {
+        public ConfigurationException(String msg) {
+            super(msg);
+        }
+    }
+
+    public EthernetNetworkFactory(Handler handler, Context context) {
+        this(handler, context, new Dependencies());
+    }
+
+    @VisibleForTesting
+    EthernetNetworkFactory(Handler handler, Context context, Dependencies deps) {
+        super(handler.getLooper(), context, NETWORK_TYPE, createDefaultNetworkCapabilities());
+
+        mHandler = handler;
+        mContext = context;
+        mDeps = deps;
+
+        setScoreFilter(NETWORK_SCORE);
+    }
+
+    @Override
+    public boolean acceptRequest(NetworkRequest request) {
+        if (DBG) {
+            Log.d(TAG, "acceptRequest, request: " + request);
+        }
+
+        return networkForRequest(request) != null;
+    }
+
+    @Override
+    protected void needNetworkFor(NetworkRequest networkRequest) {
+        NetworkInterfaceState network = networkForRequest(networkRequest);
+
+        if (network == null) {
+            Log.e(TAG, "needNetworkFor, failed to get a network for " + networkRequest);
+            return;
+        }
+
+        if (++network.refCount == 1) {
+            network.start();
+        }
+    }
+
+    @Override
+    protected void releaseNetworkFor(NetworkRequest networkRequest) {
+        NetworkInterfaceState network = networkForRequest(networkRequest);
+        if (network == null) {
+            Log.e(TAG, "releaseNetworkFor, failed to get a network for " + networkRequest);
+            return;
+        }
+
+        if (--network.refCount == 0) {
+            network.stop();
+        }
+    }
+
+    /**
+     * Returns an array of available interface names. The array is sorted: unrestricted interfaces
+     * goes first, then sorted by name.
+     */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    protected String[] getAvailableInterfaces(boolean includeRestricted) {
+        return mTrackingInterfaces.values()
+                .stream()
+                .filter(iface -> !iface.isRestricted() || includeRestricted)
+                .sorted((iface1, iface2) -> {
+                    int r = Boolean.compare(iface1.isRestricted(), iface2.isRestricted());
+                    return r == 0 ? iface1.name.compareTo(iface2.name) : r;
+                })
+                .map(iface -> iface.name)
+                .toArray(String[]::new);
+    }
+
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    protected void addInterface(@NonNull final String ifaceName, @NonNull final String hwAddress,
+            @NonNull final IpConfiguration ipConfig,
+            @NonNull final NetworkCapabilities capabilities) {
+        if (mTrackingInterfaces.containsKey(ifaceName)) {
+            Log.e(TAG, "Interface with name " + ifaceName + " already exists.");
+            return;
+        }
+
+        final NetworkCapabilities nc = new NetworkCapabilities.Builder(capabilities)
+                .setNetworkSpecifier(new EthernetNetworkSpecifier(ifaceName))
+                .build();
+
+        if (DBG) {
+            Log.d(TAG, "addInterface, iface: " + ifaceName + ", capabilities: " + nc);
+        }
+
+        final NetworkInterfaceState iface = new NetworkInterfaceState(
+                ifaceName, hwAddress, mHandler, mContext, ipConfig, nc, this, mDeps);
+        mTrackingInterfaces.put(ifaceName, iface);
+        updateCapabilityFilter();
+    }
+
+    @VisibleForTesting
+    protected int getInterfaceState(@NonNull String iface) {
+        final NetworkInterfaceState interfaceState = mTrackingInterfaces.get(iface);
+        if (interfaceState == null) {
+            return EthernetManager.STATE_ABSENT;
+        } else if (!interfaceState.mLinkUp) {
+            return EthernetManager.STATE_LINK_DOWN;
+        } else {
+            return EthernetManager.STATE_LINK_UP;
+        }
+    }
+
+    /**
+     * Update a network's configuration and restart it if necessary.
+     *
+     * @param ifaceName the interface name of the network to be updated.
+     * @param ipConfig the desired {@link IpConfiguration} for the given network or null. If
+     *                 {@code null} is passed, the existing IpConfiguration is not updated.
+     * @param capabilities the desired {@link NetworkCapabilities} for the given network. If
+     *                     {@code null} is passed, then the network's current
+     *                     {@link NetworkCapabilities} will be used in support of existing APIs as
+     *                     the public API does not allow this.
+     * @param listener an optional {@link INetworkInterfaceOutcomeReceiver} to notify callers of
+     *                 completion.
+     */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    protected void updateInterface(@NonNull final String ifaceName,
+            @Nullable final IpConfiguration ipConfig,
+            @Nullable final NetworkCapabilities capabilities,
+            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        if (!hasInterface(ifaceName)) {
+            maybeSendNetworkManagementCallbackForUntracked(ifaceName, listener);
+            return;
+        }
+
+        final NetworkInterfaceState iface = mTrackingInterfaces.get(ifaceName);
+        iface.updateInterface(ipConfig, capabilities, listener);
+        mTrackingInterfaces.put(ifaceName, iface);
+        updateCapabilityFilter();
+    }
+
+    private static NetworkCapabilities mixInCapabilities(NetworkCapabilities nc,
+            NetworkCapabilities addedNc) {
+       final NetworkCapabilities.Builder builder = new NetworkCapabilities.Builder(nc);
+       for (int transport : addedNc.getTransportTypes()) builder.addTransportType(transport);
+       for (int capability : addedNc.getCapabilities()) builder.addCapability(capability);
+       return builder.build();
+    }
+
+    private void updateCapabilityFilter() {
+        NetworkCapabilities capabilitiesFilter = createDefaultNetworkCapabilities();
+        for (NetworkInterfaceState iface:  mTrackingInterfaces.values()) {
+            capabilitiesFilter = mixInCapabilities(capabilitiesFilter, iface.mCapabilities);
+        }
+
+        if (DBG) Log.d(TAG, "updateCapabilityFilter: " + capabilitiesFilter);
+        setCapabilityFilter(capabilitiesFilter);
+    }
+
+    private static NetworkCapabilities createDefaultNetworkCapabilities() {
+        return NetworkCapabilities.Builder
+                .withoutDefaultCapabilities()
+                .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET).build();
+    }
+
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    protected void removeInterface(String interfaceName) {
+        NetworkInterfaceState iface = mTrackingInterfaces.remove(interfaceName);
+        if (iface != null) {
+            iface.maybeSendNetworkManagementCallbackForAbort();
+            iface.stop();
+        }
+
+        updateCapabilityFilter();
+    }
+
+    /** Returns true if state has been modified */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    protected boolean updateInterfaceLinkState(@NonNull final String ifaceName, final boolean up,
+            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        if (!hasInterface(ifaceName)) {
+            maybeSendNetworkManagementCallbackForUntracked(ifaceName, listener);
+            return false;
+        }
+
+        if (DBG) {
+            Log.d(TAG, "updateInterfaceLinkState, iface: " + ifaceName + ", up: " + up);
+        }
+
+        NetworkInterfaceState iface = mTrackingInterfaces.get(ifaceName);
+        return iface.updateLinkState(up, listener);
+    }
+
+    private void maybeSendNetworkManagementCallbackForUntracked(
+            String ifaceName, INetworkInterfaceOutcomeReceiver listener) {
+        maybeSendNetworkManagementCallback(listener, null,
+                new EthernetNetworkManagementException(
+                        ifaceName + " can't be updated as it is not available."));
+    }
+
+    @VisibleForTesting
+    protected boolean hasInterface(String ifaceName) {
+        return mTrackingInterfaces.containsKey(ifaceName);
+    }
+
+    private NetworkInterfaceState networkForRequest(NetworkRequest request) {
+        String requestedIface = null;
+
+        NetworkSpecifier specifier = request.getNetworkSpecifier();
+        if (specifier instanceof EthernetNetworkSpecifier) {
+            requestedIface = ((EthernetNetworkSpecifier) specifier)
+                .getInterfaceName();
+        }
+
+        NetworkInterfaceState network = null;
+        if (!TextUtils.isEmpty(requestedIface)) {
+            NetworkInterfaceState n = mTrackingInterfaces.get(requestedIface);
+            if (n != null && request.canBeSatisfiedBy(n.mCapabilities)) {
+                network = n;
+            }
+        } else {
+            for (NetworkInterfaceState n : mTrackingInterfaces.values()) {
+                if (request.canBeSatisfiedBy(n.mCapabilities) && n.mLinkUp) {
+                    network = n;
+                    break;
+                }
+            }
+        }
+
+        if (DBG) {
+            Log.i(TAG, "networkForRequest, request: " + request + ", network: " + network);
+        }
+
+        return network;
+    }
+
+    private static void maybeSendNetworkManagementCallback(
+            @Nullable final INetworkInterfaceOutcomeReceiver listener,
+            @Nullable final String iface,
+            @Nullable final EthernetNetworkManagementException e) {
+        if (null == listener) {
+            return;
+        }
+
+        try {
+            if (iface != null) {
+                listener.onResult(iface);
+            } else {
+                listener.onError(e);
+            }
+        } catch (RemoteException re) {
+            Log.e(TAG, "Can't send onComplete for network management callback", re);
+        }
+    }
+
+    @VisibleForTesting
+    static class NetworkInterfaceState {
+        final String name;
+
+        private final String mHwAddress;
+        private final Handler mHandler;
+        private final Context mContext;
+        private final NetworkFactory mNetworkFactory;
+        private final Dependencies mDeps;
+
+        private static String sTcpBufferSizes = null;  // Lazy initialized.
+
+        private boolean mLinkUp;
+        private int mLegacyType;
+        private LinkProperties mLinkProperties = new LinkProperties();
+
+        private volatile @Nullable IpClientManager mIpClient;
+        private @NonNull NetworkCapabilities mCapabilities;
+        private @Nullable EthernetIpClientCallback mIpClientCallback;
+        private @Nullable EthernetNetworkAgent mNetworkAgent;
+        private @Nullable IpConfiguration mIpConfig;
+
+        /**
+         * A map of TRANSPORT_* types to legacy transport types available for each type an ethernet
+         * interface could propagate.
+         *
+         * There are no legacy type equivalents to LOWPAN or WIFI_AWARE. These types are set to
+         * TYPE_NONE to match the behavior of their own network factories.
+         */
+        private static final SparseArray<Integer> sTransports = new SparseArray();
+        static {
+            sTransports.put(NetworkCapabilities.TRANSPORT_ETHERNET,
+                    ConnectivityManager.TYPE_ETHERNET);
+            sTransports.put(NetworkCapabilities.TRANSPORT_BLUETOOTH,
+                    ConnectivityManager.TYPE_BLUETOOTH);
+            sTransports.put(NetworkCapabilities.TRANSPORT_WIFI, ConnectivityManager.TYPE_WIFI);
+            sTransports.put(NetworkCapabilities.TRANSPORT_CELLULAR,
+                    ConnectivityManager.TYPE_MOBILE);
+            sTransports.put(NetworkCapabilities.TRANSPORT_LOWPAN, ConnectivityManager.TYPE_NONE);
+            sTransports.put(NetworkCapabilities.TRANSPORT_WIFI_AWARE,
+                    ConnectivityManager.TYPE_NONE);
+        }
+
+        long refCount = 0;
+
+        private class EthernetIpClientCallback extends IpClientCallbacks {
+            private final ConditionVariable mIpClientStartCv = new ConditionVariable(false);
+            private final ConditionVariable mIpClientShutdownCv = new ConditionVariable(false);
+            @Nullable INetworkInterfaceOutcomeReceiver mNetworkManagementListener;
+
+            EthernetIpClientCallback(@Nullable final INetworkInterfaceOutcomeReceiver listener) {
+                mNetworkManagementListener = listener;
+            }
+
+            @Override
+            public void onIpClientCreated(IIpClient ipClient) {
+                mIpClient = mDeps.makeIpClientManager(ipClient);
+                mIpClientStartCv.open();
+            }
+
+            private void awaitIpClientStart() {
+                mIpClientStartCv.block();
+            }
+
+            private void awaitIpClientShutdown() {
+                mIpClientShutdownCv.block();
+            }
+
+            // At the time IpClient is stopped, an IpClient event may have already been posted on
+            // the back of the handler and is awaiting execution. Once that event is executed, the
+            // associated callback object may not be valid anymore
+            // (NetworkInterfaceState#mIpClientCallback points to a different object / null).
+            private boolean isCurrentCallback() {
+                return this == mIpClientCallback;
+            }
+
+            private void handleIpEvent(final @NonNull Runnable r) {
+                mHandler.post(() -> {
+                    if (!isCurrentCallback()) {
+                        Log.i(TAG, "Ignoring stale IpClientCallbacks " + this);
+                        return;
+                    }
+                    r.run();
+                });
+            }
+
+            @Override
+            public void onProvisioningSuccess(LinkProperties newLp) {
+                handleIpEvent(() -> onIpLayerStarted(newLp, mNetworkManagementListener));
+            }
+
+            @Override
+            public void onProvisioningFailure(LinkProperties newLp) {
+                // This cannot happen due to provisioning timeout, because our timeout is 0. It can
+                // happen due to errors while provisioning or on provisioning loss.
+                handleIpEvent(() -> onIpLayerStopped(mNetworkManagementListener));
+            }
+
+            @Override
+            public void onLinkPropertiesChange(LinkProperties newLp) {
+                handleIpEvent(() -> updateLinkProperties(newLp));
+            }
+
+            @Override
+            public void onReachabilityLost(String logMsg) {
+                handleIpEvent(() -> updateNeighborLostEvent(logMsg));
+            }
+
+            @Override
+            public void onQuit() {
+                mIpClient = null;
+                mIpClientShutdownCv.open();
+            }
+        }
+
+        NetworkInterfaceState(String ifaceName, String hwAddress, Handler handler, Context context,
+                @NonNull IpConfiguration ipConfig, @NonNull NetworkCapabilities capabilities,
+                NetworkFactory networkFactory, Dependencies deps) {
+            name = ifaceName;
+            mIpConfig = Objects.requireNonNull(ipConfig);
+            mCapabilities = Objects.requireNonNull(capabilities);
+            mLegacyType = getLegacyType(mCapabilities);
+            mHandler = handler;
+            mContext = context;
+            mNetworkFactory = networkFactory;
+            mDeps = deps;
+            mHwAddress = hwAddress;
+        }
+
+        /**
+         * Determines the legacy transport type from a NetworkCapabilities transport type. Defaults
+         * to legacy TYPE_NONE if there is no known conversion
+         */
+        private static int getLegacyType(int transport) {
+            return sTransports.get(transport, ConnectivityManager.TYPE_NONE);
+        }
+
+        private static int getLegacyType(@NonNull final NetworkCapabilities capabilities) {
+            final int[] transportTypes = capabilities.getTransportTypes();
+            if (transportTypes.length > 0) {
+                return getLegacyType(transportTypes[0]);
+            }
+
+            // Should never happen as transport is always one of ETHERNET or a valid override
+            throw new ConfigurationException("Network Capabilities do not have an associated "
+                    + "transport type.");
+        }
+
+        private void setCapabilities(@NonNull final NetworkCapabilities capabilities) {
+            mCapabilities = new NetworkCapabilities(capabilities);
+            mLegacyType = getLegacyType(mCapabilities);
+        }
+
+        void updateInterface(@Nullable final IpConfiguration ipConfig,
+                @Nullable final NetworkCapabilities capabilities,
+                @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+            if (DBG) {
+                Log.d(TAG, "updateInterface, iface: " + name
+                        + ", ipConfig: " + ipConfig + ", old ipConfig: " + mIpConfig
+                        + ", capabilities: " + capabilities + ", old capabilities: " + mCapabilities
+                        + ", listener: " + listener
+                );
+            }
+
+            if (null != ipConfig){
+                mIpConfig = ipConfig;
+            }
+            if (null != capabilities) {
+                setCapabilities(capabilities);
+            }
+            // Send an abort callback if a request is filed before the previous one has completed.
+            maybeSendNetworkManagementCallbackForAbort();
+            // TODO: Update this logic to only do a restart if required. Although a restart may
+            //  be required due to the capabilities or ipConfiguration values, not all
+            //  capabilities changes require a restart.
+            restart(listener);
+        }
+
+        boolean isRestricted() {
+            return !mCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
+        }
+
+        private void start() {
+            start(null);
+        }
+
+        private void start(@Nullable final INetworkInterfaceOutcomeReceiver listener) {
+            if (mIpClient != null) {
+                if (DBG) Log.d(TAG, "IpClient already started");
+                return;
+            }
+            if (DBG) {
+                Log.d(TAG, String.format("Starting Ethernet IpClient(%s)", name));
+            }
+
+            mIpClientCallback = new EthernetIpClientCallback(listener);
+            mDeps.makeIpClient(mContext, name, mIpClientCallback);
+            mIpClientCallback.awaitIpClientStart();
+
+            if (sTcpBufferSizes == null) {
+                sTcpBufferSizes = mDeps.getTcpBufferSizesFromResource(mContext);
+            }
+            provisionIpClient(mIpClient, mIpConfig, sTcpBufferSizes);
+        }
+
+        void onIpLayerStarted(@NonNull final LinkProperties linkProperties,
+                @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+            if (mNetworkAgent != null) {
+                Log.e(TAG, "Already have a NetworkAgent - aborting new request");
+                stop();
+                return;
+            }
+            mLinkProperties = linkProperties;
+
+            // Create our NetworkAgent.
+            final NetworkAgentConfig config = new NetworkAgentConfig.Builder()
+                    .setLegacyType(mLegacyType)
+                    .setLegacyTypeName(NETWORK_TYPE)
+                    .setLegacyExtraInfo(mHwAddress)
+                    .build();
+            mNetworkAgent = mDeps.makeEthernetNetworkAgent(mContext, mHandler.getLooper(),
+                    mCapabilities, mLinkProperties, config, mNetworkFactory.getProvider(),
+                    new EthernetNetworkAgent.Callbacks() {
+                        @Override
+                        public void onNetworkUnwanted() {
+                            // if mNetworkAgent is null, we have already called stop.
+                            if (mNetworkAgent == null) return;
+
+                            if (this == mNetworkAgent.getCallbacks()) {
+                                stop();
+                            } else {
+                                Log.d(TAG, "Ignoring unwanted as we have a more modern " +
+                                        "instance");
+                            }
+                        }
+                    });
+            mNetworkAgent.register();
+            mNetworkAgent.markConnected();
+            realizeNetworkManagementCallback(name, null);
+        }
+
+        void onIpLayerStopped(@Nullable final INetworkInterfaceOutcomeReceiver listener) {
+            // There is no point in continuing if the interface is gone as stop() will be triggered
+            // by removeInterface() when processed on the handler thread and start() won't
+            // work for a non-existent interface.
+            if (null == mDeps.getNetworkInterfaceByName(name)) {
+                if (DBG) Log.d(TAG, name + " is no longer available.");
+                // Send a callback in case a provisioning request was in progress.
+                maybeSendNetworkManagementCallbackForAbort();
+                return;
+            }
+            restart(listener);
+        }
+
+        private void maybeSendNetworkManagementCallbackForAbort() {
+            realizeNetworkManagementCallback(null,
+                    new EthernetNetworkManagementException(
+                            "The IP provisioning request has been aborted."));
+        }
+
+        // Must be called on the handler thread
+        private void realizeNetworkManagementCallback(@Nullable final String iface,
+                @Nullable final EthernetNetworkManagementException e) {
+            ensureRunningOnEthernetHandlerThread();
+            if (null == mIpClientCallback) {
+                return;
+            }
+
+            EthernetNetworkFactory.maybeSendNetworkManagementCallback(
+                    mIpClientCallback.mNetworkManagementListener, iface, e);
+            // Only send a single callback per listener.
+            mIpClientCallback.mNetworkManagementListener = null;
+        }
+
+        private void ensureRunningOnEthernetHandlerThread() {
+            if (mHandler.getLooper().getThread() != Thread.currentThread()) {
+                throw new IllegalStateException(
+                        "Not running on the Ethernet thread: "
+                                + Thread.currentThread().getName());
+            }
+        }
+
+        void updateLinkProperties(LinkProperties linkProperties) {
+            mLinkProperties = linkProperties;
+            if (mNetworkAgent != null) {
+                mNetworkAgent.sendLinkPropertiesImpl(linkProperties);
+            }
+        }
+
+        void updateNeighborLostEvent(String logMsg) {
+            Log.i(TAG, "updateNeighborLostEvent " + logMsg);
+            // Reachability lost will be seen only if the gateway is not reachable.
+            // Since ethernet FW doesn't have the mechanism to scan for new networks
+            // like WiFi, simply restart.
+            // If there is a better network, that will become default and apps
+            // will be able to use internet. If ethernet gets connected again,
+            // and has backhaul connectivity, it will become default.
+            restart();
+        }
+
+        /** Returns true if state has been modified */
+        boolean updateLinkState(final boolean up,
+                @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+            if (mLinkUp == up)  {
+                EthernetNetworkFactory.maybeSendNetworkManagementCallback(listener, null,
+                        new EthernetNetworkManagementException(
+                                "No changes with requested link state " + up + " for " + name));
+                return false;
+            }
+            mLinkUp = up;
+
+            if (!up) { // was up, goes down
+                // Send an abort on a provisioning request callback if necessary before stopping.
+                maybeSendNetworkManagementCallbackForAbort();
+                stop();
+                // If only setting the interface down, send a callback to signal completion.
+                EthernetNetworkFactory.maybeSendNetworkManagementCallback(listener, name, null);
+            } else { // was down, goes up
+                stop();
+                start(listener);
+            }
+
+            return true;
+        }
+
+        void stop() {
+            // Invalidate all previous start requests
+            if (mIpClient != null) {
+                mIpClient.shutdown();
+                mIpClientCallback.awaitIpClientShutdown();
+                mIpClient = null;
+            }
+            mIpClientCallback = null;
+
+            if (mNetworkAgent != null) {
+                mNetworkAgent.unregister();
+                mNetworkAgent = null;
+            }
+            mLinkProperties.clear();
+        }
+
+        private static void provisionIpClient(@NonNull final IpClientManager ipClient,
+                @NonNull final IpConfiguration config, @NonNull final String tcpBufferSizes) {
+            if (config.getProxySettings() == ProxySettings.STATIC ||
+                    config.getProxySettings() == ProxySettings.PAC) {
+                ipClient.setHttpProxy(config.getHttpProxy());
+            }
+
+            if (!TextUtils.isEmpty(tcpBufferSizes)) {
+                ipClient.setTcpBufferSizes(tcpBufferSizes);
+            }
+
+            ipClient.startProvisioning(createProvisioningConfiguration(config));
+        }
+
+        private static ProvisioningConfiguration createProvisioningConfiguration(
+                @NonNull final IpConfiguration config) {
+            if (config.getIpAssignment() == IpAssignment.STATIC) {
+                return new ProvisioningConfiguration.Builder()
+                        .withStaticConfiguration(config.getStaticIpConfiguration())
+                        .build();
+            }
+            return new ProvisioningConfiguration.Builder()
+                        .withProvisioningTimeoutMs(0)
+                        .build();
+        }
+
+        void restart() {
+            restart(null);
+        }
+
+        void restart(@Nullable final INetworkInterfaceOutcomeReceiver listener) {
+            if (DBG) Log.d(TAG, "reconnecting Ethernet");
+            stop();
+            start(listener);
+        }
+
+        @Override
+        public String toString() {
+            return getClass().getSimpleName() + "{ "
+                    + "refCount: " + refCount + ", "
+                    + "iface: " + name + ", "
+                    + "up: " + mLinkUp + ", "
+                    + "hwAddress: " + mHwAddress + ", "
+                    + "networkCapabilities: " + mCapabilities + ", "
+                    + "networkAgent: " + mNetworkAgent + ", "
+                    + "ipClient: " + mIpClient + ","
+                    + "linkProperties: " + mLinkProperties
+                    + "}";
+        }
+    }
+
+    void dump(FileDescriptor fd, IndentingPrintWriter pw, String[] args) {
+        super.dump(fd, pw, args);
+        pw.println(getClass().getSimpleName());
+        pw.println("Tracking interfaces:");
+        pw.increaseIndent();
+        for (String iface: mTrackingInterfaces.keySet()) {
+            NetworkInterfaceState ifaceState = mTrackingInterfaces.get(iface);
+            pw.println(iface + ":" + ifaceState);
+            pw.increaseIndent();
+            if (null == ifaceState.mIpClient) {
+                pw.println("IpClient is null");
+            }
+            pw.decreaseIndent();
+        }
+        pw.decreaseIndent();
+    }
+}
diff --git a/service-t/src/com/android/server/ethernet/EthernetService.java b/service-t/src/com/android/server/ethernet/EthernetService.java
new file mode 100644
index 0000000..d405fd5
--- /dev/null
+++ b/service-t/src/com/android/server/ethernet/EthernetService.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2014 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.ethernet;
+
+import android.content.Context;
+import android.net.INetd;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+
+import java.util.Objects;
+
+// TODO: consider renaming EthernetServiceImpl to EthernetService and deleting this file.
+public final class EthernetService {
+    private static final String TAG = "EthernetService";
+    private static final String THREAD_NAME = "EthernetServiceThread";
+
+    private static INetd getNetd(Context context) {
+        final INetd netd =
+                INetd.Stub.asInterface((IBinder) context.getSystemService(Context.NETD_SERVICE));
+        Objects.requireNonNull(netd, "could not get netd instance");
+        return netd;
+    }
+
+    public static EthernetServiceImpl create(Context context) {
+        final HandlerThread handlerThread = new HandlerThread(THREAD_NAME);
+        handlerThread.start();
+        final Handler handler = new Handler(handlerThread.getLooper());
+        final EthernetNetworkFactory factory = new EthernetNetworkFactory(handler, context);
+        return new EthernetServiceImpl(context, handler,
+                new EthernetTracker(context, handler, factory, getNetd(context)));
+    }
+}
diff --git a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
new file mode 100644
index 0000000..5e830ad
--- /dev/null
+++ b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2014 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.ethernet;
+
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.IEthernetManager;
+import android.net.IEthernetServiceListener;
+import android.net.INetworkInterfaceOutcomeReceiver;
+import android.net.ITetheredInterfaceCallback;
+import android.net.EthernetNetworkUpdateRequest;
+import android.net.IpConfiguration;
+import android.net.NetworkCapabilities;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.PrintWriterPrinter;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.net.module.util.PermissionUtils;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * EthernetServiceImpl handles remote Ethernet operation requests by implementing
+ * the IEthernetManager interface.
+ */
+public class EthernetServiceImpl extends IEthernetManager.Stub {
+    private static final String TAG = "EthernetServiceImpl";
+
+    @VisibleForTesting
+    final AtomicBoolean mStarted = new AtomicBoolean(false);
+    private final Context mContext;
+    private final Handler mHandler;
+    private final EthernetTracker mTracker;
+
+    EthernetServiceImpl(@NonNull final Context context, @NonNull final Handler handler,
+            @NonNull final EthernetTracker tracker) {
+        mContext = context;
+        mHandler = handler;
+        mTracker = tracker;
+    }
+
+    private void enforceAutomotiveDevice(final @NonNull String methodName) {
+        PermissionUtils.enforceSystemFeature(mContext, PackageManager.FEATURE_AUTOMOTIVE,
+                methodName + " is only available on automotive devices.");
+    }
+
+    private boolean checkUseRestrictedNetworksPermission() {
+        return PermissionUtils.checkAnyPermissionOf(mContext,
+                android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS);
+    }
+
+    public void start() {
+        Log.i(TAG, "Starting Ethernet service");
+        mTracker.start();
+        mStarted.set(true);
+    }
+
+    private void throwIfEthernetNotStarted() {
+        if (!mStarted.get()) {
+            throw new IllegalStateException("System isn't ready to change ethernet configurations");
+        }
+    }
+
+    @Override
+    public String[] getAvailableInterfaces() throws RemoteException {
+        PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG);
+        return mTracker.getInterfaces(checkUseRestrictedNetworksPermission());
+    }
+
+    /**
+     * Get Ethernet configuration
+     * @return the Ethernet Configuration, contained in {@link IpConfiguration}.
+     */
+    @Override
+    public IpConfiguration getConfiguration(String iface) {
+        PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG);
+        if (mTracker.isRestrictedInterface(iface)) {
+            PermissionUtils.enforceRestrictedNetworkPermission(mContext, TAG);
+        }
+
+        return new IpConfiguration(mTracker.getIpConfiguration(iface));
+    }
+
+    /**
+     * Set Ethernet configuration
+     */
+    @Override
+    public void setConfiguration(String iface, IpConfiguration config) {
+        throwIfEthernetNotStarted();
+
+        PermissionUtils.enforceNetworkStackPermission(mContext);
+        if (mTracker.isRestrictedInterface(iface)) {
+            PermissionUtils.enforceRestrictedNetworkPermission(mContext, TAG);
+        }
+
+        // TODO: this does not check proxy settings, gateways, etc.
+        // Fix this by making IpConfiguration a complete representation of static configuration.
+        mTracker.updateIpConfiguration(iface, new IpConfiguration(config));
+    }
+
+    /**
+     * Indicates whether given interface is available.
+     */
+    @Override
+    public boolean isAvailable(String iface) {
+        PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG);
+        if (mTracker.isRestrictedInterface(iface)) {
+            PermissionUtils.enforceRestrictedNetworkPermission(mContext, TAG);
+        }
+
+        return mTracker.isTrackingInterface(iface);
+    }
+
+    /**
+     * Adds a listener.
+     * @param listener A {@link IEthernetServiceListener} to add.
+     */
+    public void addListener(IEthernetServiceListener listener) throws RemoteException {
+        Objects.requireNonNull(listener, "listener must not be null");
+        PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG);
+        mTracker.addListener(listener, checkUseRestrictedNetworksPermission());
+    }
+
+    /**
+     * Removes a listener.
+     * @param listener A {@link IEthernetServiceListener} to remove.
+     */
+    public void removeListener(IEthernetServiceListener listener) {
+        if (listener == null) {
+            throw new IllegalArgumentException("listener must not be null");
+        }
+        PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG);
+        mTracker.removeListener(listener);
+    }
+
+    @Override
+    public void setIncludeTestInterfaces(boolean include) {
+        PermissionUtils.enforceNetworkStackPermissionOr(mContext,
+                android.Manifest.permission.NETWORK_SETTINGS);
+        mTracker.setIncludeTestInterfaces(include);
+    }
+
+    @Override
+    public void requestTetheredInterface(ITetheredInterfaceCallback callback) {
+        Objects.requireNonNull(callback, "callback must not be null");
+        PermissionUtils.enforceNetworkStackPermissionOr(mContext,
+                android.Manifest.permission.NETWORK_SETTINGS);
+        mTracker.requestTetheredInterface(callback);
+    }
+
+    @Override
+    public void releaseTetheredInterface(ITetheredInterfaceCallback callback) {
+        Objects.requireNonNull(callback, "callback must not be null");
+        PermissionUtils.enforceNetworkStackPermissionOr(mContext,
+                android.Manifest.permission.NETWORK_SETTINGS);
+        mTracker.releaseTetheredInterface(callback);
+    }
+
+    @Override
+    protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+        final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
+        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
+                != PackageManager.PERMISSION_GRANTED) {
+            pw.println("Permission Denial: can't dump EthernetService from pid="
+                    + Binder.getCallingPid()
+                    + ", uid=" + Binder.getCallingUid());
+            return;
+        }
+
+        pw.println("Current Ethernet state: ");
+        pw.increaseIndent();
+        mTracker.dump(fd, pw, args);
+        pw.decreaseIndent();
+
+        pw.println("Handler:");
+        pw.increaseIndent();
+        mHandler.dump(new PrintWriterPrinter(pw), "EthernetServiceImpl");
+        pw.decreaseIndent();
+    }
+
+    private void enforceNetworkManagementPermission() {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.MANAGE_ETHERNET_NETWORKS,
+                "EthernetServiceImpl");
+    }
+
+    private void enforceManageTestNetworksPermission() {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.MANAGE_TEST_NETWORKS,
+                "EthernetServiceImpl");
+    }
+
+    private void maybeValidateTestCapabilities(final String iface,
+            @Nullable final NetworkCapabilities nc) {
+        if (!mTracker.isValidTestInterface(iface)) {
+            return;
+        }
+        // For test interfaces, only null or capabilities that include TRANSPORT_TEST are
+        // allowed.
+        if (nc != null && !nc.hasTransport(TRANSPORT_TEST)) {
+            throw new IllegalArgumentException(
+                    "Updates to test interfaces must have NetworkCapabilities.TRANSPORT_TEST.");
+        }
+    }
+
+    private void enforceAdminPermission(final String iface, boolean enforceAutomotive,
+            final String logMessage) {
+        if (mTracker.isValidTestInterface(iface)) {
+            enforceManageTestNetworksPermission();
+        } else {
+            enforceNetworkManagementPermission();
+            if (enforceAutomotive) {
+                enforceAutomotiveDevice(logMessage);
+            }
+        }
+    }
+
+    @Override
+    public void updateConfiguration(@NonNull final String iface,
+            @NonNull final EthernetNetworkUpdateRequest request,
+            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        Objects.requireNonNull(iface);
+        Objects.requireNonNull(request);
+        throwIfEthernetNotStarted();
+
+        // TODO: validate that iface is listed in overlay config_ethernet_interfaces
+        // only automotive devices are allowed to set the NetworkCapabilities using this API
+        enforceAdminPermission(iface, request.getNetworkCapabilities() != null,
+                "updateConfiguration() with non-null capabilities");
+        maybeValidateTestCapabilities(iface, request.getNetworkCapabilities());
+
+        mTracker.updateConfiguration(
+                iface, request.getIpConfiguration(), request.getNetworkCapabilities(), listener);
+    }
+
+    @Override
+    public void connectNetwork(@NonNull final String iface,
+            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        Log.i(TAG, "connectNetwork called with: iface=" + iface + ", listener=" + listener);
+        Objects.requireNonNull(iface);
+        throwIfEthernetNotStarted();
+
+        enforceAdminPermission(iface, true, "connectNetwork()");
+
+        mTracker.connectNetwork(iface, listener);
+    }
+
+    @Override
+    public void disconnectNetwork(@NonNull final String iface,
+            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        Log.i(TAG, "disconnectNetwork called with: iface=" + iface + ", listener=" + listener);
+        Objects.requireNonNull(iface);
+        throwIfEthernetNotStarted();
+
+        enforceAdminPermission(iface, true, "connectNetwork()");
+
+        mTracker.disconnectNetwork(iface, listener);
+    }
+
+    @Override
+    public void setEthernetEnabled(boolean enabled) {
+        PermissionUtils.enforceNetworkStackPermissionOr(mContext,
+                android.Manifest.permission.NETWORK_SETTINGS);
+
+        mTracker.setEthernetEnabled(enabled);
+    }
+
+    @Override
+    public List<String> getInterfaceList() {
+        PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG);
+        return mTracker.getInterfaceList();
+    }
+}
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
new file mode 100644
index 0000000..693d91a
--- /dev/null
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -0,0 +1,966 @@
+/*
+ * 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 com.android.server.ethernet;
+
+import static android.net.EthernetManager.ETHERNET_STATE_DISABLED;
+import static android.net.EthernetManager.ETHERNET_STATE_ENABLED;
+import static android.net.TestNetworkManager.TEST_TAP_PREFIX;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+
+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.INetd;
+import android.net.INetworkInterfaceOutcomeReceiver;
+import android.net.ITetheredInterfaceCallback;
+import android.net.InterfaceConfigurationParcel;
+import android.net.IpConfiguration;
+import android.net.IpConfiguration.IpAssignment;
+import android.net.IpConfiguration.ProxySettings;
+import android.net.LinkAddress;
+import android.net.NetworkCapabilities;
+import android.net.StaticIpConfiguration;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
+import com.android.net.module.util.NetdUtils;
+import com.android.net.module.util.PermissionUtils;
+
+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;
+
+/**
+ * Tracks Ethernet interfaces and manages interface configurations.
+ *
+ * <p>Interfaces may have different {@link android.net.NetworkCapabilities}. This mapping is defined
+ * in {@code config_ethernet_interfaces}. Notably, some interfaces could be marked as restricted by
+ * not specifying {@link android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED} flag.
+ * Interfaces could have associated {@link android.net.IpConfiguration}.
+ * Ethernet Interfaces may be present at boot time or appear after boot (e.g., for Ethernet adapters
+ * connected over USB). This class supports multiple interfaces. When an interface appears on the
+ * system (or is present at boot time) this class will start tracking it and bring it up. Only
+ * interfaces whose names match the {@code config_ethernet_iface_regex} regular expression are
+ * tracked.
+ *
+ * <p>All public or package private methods must be thread-safe unless stated otherwise.
+ */
+@VisibleForTesting(visibility = PACKAGE)
+public class EthernetTracker {
+    private static final int INTERFACE_MODE_CLIENT = 1;
+    private static final int INTERFACE_MODE_SERVER = 2;
+
+    private static final String TAG = EthernetTracker.class.getSimpleName();
+    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,
+     * if setIncludeTestInterfaces is true, any test interfaces.
+     */
+    private volatile String mIfaceMatch;
+    /**
+     * Track test interfaces if true, don't track otherwise.
+     */
+    private boolean mIncludeTestInterfaces = false;
+
+    /** Mapping between {iface name | mac address} -> {NetworkCapabilities} */
+    private final ConcurrentHashMap<String, NetworkCapabilities> mNetworkCapabilities =
+            new ConcurrentHashMap<>();
+    private final ConcurrentHashMap<String, IpConfiguration> mIpConfigurations =
+            new ConcurrentHashMap<>();
+
+    private final Context mContext;
+    private final INetd mNetd;
+    private final Handler mHandler;
+    private final EthernetNetworkFactory mFactory;
+    private final EthernetConfigStore mConfigStore;
+    private final Dependencies mDeps;
+
+    private final RemoteCallbackList<IEthernetServiceListener> mListeners =
+            new RemoteCallbackList<>();
+    private final TetheredInterfaceRequestList mTetheredInterfaceRequests =
+            new TetheredInterfaceRequestList();
+
+    // Used only on the handler thread
+    private String mDefaultInterface;
+    private int mDefaultInterfaceMode = INTERFACE_MODE_CLIENT;
+    // Tracks whether clients were notified that the tethered interface is available
+    private boolean mTetheredInterfaceWasAvailable = false;
+    private volatile IpConfiguration mIpConfigForDefaultInterface;
+
+    private int mEthernetState = ETHERNET_STATE_ENABLED;
+
+    private class TetheredInterfaceRequestList extends
+            RemoteCallbackList<ITetheredInterfaceCallback> {
+        @Override
+        public void onCallbackDied(ITetheredInterfaceCallback cb, Object cookie) {
+            mHandler.post(EthernetTracker.this::maybeUntetherDefaultInterface);
+        }
+    }
+
+    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;
+        }
+
+        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;
+        }
+    }
+
+    EthernetTracker(@NonNull final Context context, @NonNull final Handler handler,
+            @NonNull final EthernetNetworkFactory factory, @NonNull final INetd netd) {
+        this(context, handler, factory, netd, new Dependencies());
+    }
+
+    @VisibleForTesting
+    EthernetTracker(@NonNull final Context context, @NonNull final Handler handler,
+            @NonNull final EthernetNetworkFactory factory, @NonNull final INetd netd,
+            @NonNull final Dependencies deps) {
+        mContext = context;
+        mHandler = handler;
+        mFactory = factory;
+        mNetd = netd;
+        mDeps = deps;
+
+        // Interface match regex.
+        updateIfaceMatchRegexp();
+
+        // Read default Ethernet interface configuration from resources
+        final String[] interfaceConfigs = mDeps.getInterfaceConfigFromResource(context);
+        for (String strConfig : interfaceConfigs) {
+            parseEthernetConfig(strConfig);
+        }
+
+        mConfigStore = new EthernetConfigStore();
+    }
+
+    void start() {
+        mFactory.register();
+        mConfigStore.read();
+
+        // Default interface is just the first one we want to track.
+        mIpConfigForDefaultInterface = mConfigStore.getIpConfigurationForDefaultInterface();
+        final ArrayMap<String, IpConfiguration> configs = mConfigStore.getIpConfigurations();
+        for (int i = 0; i < configs.size(); i++) {
+            mIpConfigurations.put(configs.keyAt(i), configs.valueAt(i));
+        }
+
+        try {
+            PermissionUtils.enforceNetworkStackPermission(mContext);
+            mNetd.registerUnsolicitedEventListener(new InterfaceObserver());
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "Could not register InterfaceObserver " + e);
+        }
+
+        mHandler.post(this::trackAvailableInterfaces);
+    }
+
+    void updateIpConfiguration(String iface, IpConfiguration ipConfiguration) {
+        if (DBG) {
+            Log.i(TAG, "updateIpConfiguration, iface: " + iface + ", cfg: " + ipConfiguration);
+        }
+        writeIpConfiguration(iface, ipConfiguration);
+        mHandler.post(() -> {
+            mFactory.updateInterface(iface, ipConfiguration, null, null);
+            broadcastInterfaceStateChange(iface);
+        });
+    }
+
+    private void writeIpConfiguration(@NonNull final String iface,
+            @NonNull final IpConfiguration ipConfig) {
+        mConfigStore.write(iface, ipConfig);
+        mIpConfigurations.put(iface, ipConfig);
+    }
+
+    private IpConfiguration getIpConfigurationForCallback(String iface, int state) {
+        return (state == EthernetManager.STATE_ABSENT) ? null : getOrCreateIpConfiguration(iface);
+    }
+
+    private void ensureRunningOnEthernetServiceThread() {
+        if (mHandler.getLooper().getThread() != Thread.currentThread()) {
+            throw new IllegalStateException(
+                    "Not running on EthernetService thread: "
+                            + Thread.currentThread().getName());
+        }
+    }
+
+    /**
+     * Broadcast the link state or IpConfiguration change of existing Ethernet interfaces to all
+     * listeners.
+     */
+    protected void broadcastInterfaceStateChange(@NonNull String iface) {
+        ensureRunningOnEthernetServiceThread();
+        final int state = mFactory.getInterfaceState(iface);
+        final int role = getInterfaceRole(iface);
+        final IpConfiguration config = getIpConfigurationForCallback(iface, state);
+        final int n = mListeners.beginBroadcast();
+        for (int i = 0; i < n; i++) {
+            try {
+                mListeners.getBroadcastItem(i).onInterfaceStateChanged(iface, state, role, config);
+            } catch (RemoteException e) {
+                // Do nothing here.
+            }
+        }
+        mListeners.finishBroadcast();
+    }
+
+    /**
+     * Unicast the interface state or IpConfiguration change of existing Ethernet interfaces to a
+     * specific listener.
+     */
+    protected void unicastInterfaceStateChange(@NonNull IEthernetServiceListener listener,
+            @NonNull String iface) {
+        ensureRunningOnEthernetServiceThread();
+        final int state = mFactory.getInterfaceState(iface);
+        final int role = getInterfaceRole(iface);
+        final IpConfiguration config = getIpConfigurationForCallback(iface, state);
+        try {
+            listener.onInterfaceStateChanged(iface, state, role, config);
+        } catch (RemoteException e) {
+            // Do nothing here.
+        }
+    }
+
+    @VisibleForTesting(visibility = PACKAGE)
+    protected void updateConfiguration(@NonNull final String iface,
+            @Nullable final IpConfiguration ipConfig,
+            @Nullable final NetworkCapabilities capabilities,
+            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        if (DBG) {
+            Log.i(TAG, "updateConfiguration, iface: " + iface + ", capabilities: " + capabilities
+                    + ", ipConfig: " + ipConfig);
+        }
+
+        final IpConfiguration localIpConfig = ipConfig == null
+                ? null : new IpConfiguration(ipConfig);
+        if (ipConfig != null) {
+            writeIpConfiguration(iface, localIpConfig);
+        }
+
+        if (null != capabilities) {
+            mNetworkCapabilities.put(iface, capabilities);
+        }
+        mHandler.post(() -> {
+            mFactory.updateInterface(iface, localIpConfig, capabilities, listener);
+            broadcastInterfaceStateChange(iface);
+        });
+    }
+
+    @VisibleForTesting(visibility = PACKAGE)
+    protected void connectNetwork(@NonNull final String iface,
+            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        mHandler.post(() -> updateInterfaceState(iface, true, listener));
+    }
+
+    @VisibleForTesting(visibility = PACKAGE)
+    protected void disconnectNetwork(@NonNull final String iface,
+            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        mHandler.post(() -> updateInterfaceState(iface, false, listener));
+    }
+
+    IpConfiguration getIpConfiguration(String iface) {
+        return mIpConfigurations.get(iface);
+    }
+
+    @VisibleForTesting(visibility = PACKAGE)
+    protected boolean isTrackingInterface(String iface) {
+        return mFactory.hasInterface(iface);
+    }
+
+    String[] getInterfaces(boolean includeRestricted) {
+        return mFactory.getAvailableInterfaces(includeRestricted);
+    }
+
+    List<String> getInterfaceList() {
+        final List<String> interfaceList = new ArrayList<String>();
+        final String[] ifaces;
+        try {
+            ifaces = mNetd.interfaceGetList();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Could not get list of interfaces " + e);
+            return interfaceList;
+        }
+        final String ifaceMatch = mIfaceMatch;
+        for (String iface : ifaces) {
+            if (iface.matches(ifaceMatch)) interfaceList.add(iface);
+        }
+        return interfaceList;
+    }
+
+    /**
+     * Returns true if given interface was configured as restricted (doesn't have
+     * NET_CAPABILITY_NOT_RESTRICTED) capability. Otherwise, returns false.
+     */
+    boolean isRestrictedInterface(String iface) {
+        final NetworkCapabilities nc = mNetworkCapabilities.get(iface);
+        return nc != null && !nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
+    }
+
+    void addListener(IEthernetServiceListener listener, boolean canUseRestrictedNetworks) {
+        mHandler.post(() -> {
+            if (!mListeners.register(listener, new ListenerInfo(canUseRestrictedNetworks))) {
+                // Remote process has already died
+                return;
+            }
+            for (String iface : getInterfaces(canUseRestrictedNetworks)) {
+                unicastInterfaceStateChange(listener, iface);
+            }
+
+            unicastEthernetStateChange(listener, mEthernetState);
+        });
+    }
+
+    void removeListener(IEthernetServiceListener listener) {
+        mHandler.post(() -> mListeners.unregister(listener));
+    }
+
+    public void setIncludeTestInterfaces(boolean include) {
+        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)) {
+                // Remote process has already died
+                return;
+            }
+            if (mDefaultInterfaceMode == INTERFACE_MODE_SERVER) {
+                if (mTetheredInterfaceWasAvailable) {
+                    notifyTetheredInterfaceAvailable(callback, mDefaultInterface);
+                }
+                return;
+            }
+
+            setDefaultInterfaceMode(INTERFACE_MODE_SERVER);
+        });
+    }
+
+    public void releaseTetheredInterface(ITetheredInterfaceCallback callback) {
+        mHandler.post(() -> {
+            mTetheredInterfaceRequests.unregister(callback);
+            maybeUntetherDefaultInterface();
+        });
+    }
+
+    private void notifyTetheredInterfaceAvailable(ITetheredInterfaceCallback cb, String iface) {
+        try {
+            cb.onAvailable(iface);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error sending tethered interface available callback", e);
+        }
+    }
+
+    private void notifyTetheredInterfaceUnavailable(ITetheredInterfaceCallback cb) {
+        try {
+            cb.onUnavailable();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error sending tethered interface available callback", e);
+        }
+    }
+
+    private void maybeUntetherDefaultInterface() {
+        if (mTetheredInterfaceRequests.getRegisteredCallbackCount() > 0) return;
+        if (mDefaultInterfaceMode == INTERFACE_MODE_CLIENT) return;
+        setDefaultInterfaceMode(INTERFACE_MODE_CLIENT);
+    }
+
+    private void setDefaultInterfaceMode(int mode) {
+        Log.d(TAG, "Setting default interface mode to " + mode);
+        mDefaultInterfaceMode = mode;
+        if (mDefaultInterface != null) {
+            removeInterface(mDefaultInterface);
+            addInterface(mDefaultInterface);
+        }
+    }
+
+    private int getInterfaceRole(final String iface) {
+        if (!mFactory.hasInterface(iface)) return EthernetManager.ROLE_NONE;
+        final int mode = getInterfaceMode(iface);
+        return (mode == INTERFACE_MODE_CLIENT)
+                ? EthernetManager.ROLE_CLIENT
+                : EthernetManager.ROLE_SERVER;
+    }
+
+    private int getInterfaceMode(final String iface) {
+        if (iface.equals(mDefaultInterface)) {
+            return mDefaultInterfaceMode;
+        }
+        return INTERFACE_MODE_CLIENT;
+    }
+
+    private void removeInterface(String iface) {
+        mFactory.removeInterface(iface);
+        maybeUpdateServerModeInterfaceState(iface, false);
+    }
+
+    private void stopTrackingInterface(String iface) {
+        removeInterface(iface);
+        if (iface.equals(mDefaultInterface)) {
+            mDefaultInterface = null;
+        }
+        broadcastInterfaceStateChange(iface);
+    }
+
+    private void addInterface(String iface) {
+        InterfaceConfigurationParcel config = null;
+        // Bring up the interface so we get link status indications.
+        try {
+            PermissionUtils.enforceNetworkStackPermission(mContext);
+            NetdUtils.setInterfaceUp(mNetd, iface);
+            config = NetdUtils.getInterfaceConfigParcel(mNetd, iface);
+        } catch (IllegalStateException e) {
+            // Either the system is crashing or the interface has disappeared. Just ignore the
+            // error; we haven't modified any state because we only do that if our calls succeed.
+            Log.e(TAG, "Error upping interface " + iface, e);
+        }
+
+        if (config == null) {
+            Log.e(TAG, "Null interface config parcelable for " + iface + ". Bailing out.");
+            return;
+        }
+
+        final String hwAddress = config.hwAddr;
+
+        NetworkCapabilities nc = mNetworkCapabilities.get(iface);
+        if (nc == null) {
+            // Try to resolve using mac address
+            nc = mNetworkCapabilities.get(hwAddress);
+            if (nc == null) {
+                final boolean isTestIface = iface.matches(TEST_IFACE_REGEXP);
+                nc = createDefaultNetworkCapabilities(isTestIface);
+            }
+        }
+
+        final int mode = getInterfaceMode(iface);
+        if (mode == INTERFACE_MODE_CLIENT) {
+            IpConfiguration ipConfiguration = getOrCreateIpConfiguration(iface);
+            Log.d(TAG, "Tracking interface in client mode: " + iface);
+            mFactory.addInterface(iface, hwAddress, ipConfiguration, nc);
+        } else {
+            maybeUpdateServerModeInterfaceState(iface, true);
+        }
+
+        // Note: if the interface already has link (e.g., if we crashed and got
+        // restarted while it was running), we need to fake a link up notification so we
+        // start configuring it.
+        if (NetdUtils.hasFlag(config, "running")) {
+            updateInterfaceState(iface, true);
+        }
+    }
+
+    private void updateInterfaceState(String iface, boolean up) {
+        updateInterfaceState(iface, up, null /* listener */);
+    }
+
+    private void updateInterfaceState(@NonNull final String iface, final boolean up,
+            @Nullable final INetworkInterfaceOutcomeReceiver listener) {
+        final int mode = getInterfaceMode(iface);
+        final boolean factoryLinkStateUpdated = (mode == INTERFACE_MODE_CLIENT)
+                && mFactory.updateInterfaceLinkState(iface, up, listener);
+
+        if (factoryLinkStateUpdated) {
+            broadcastInterfaceStateChange(iface);
+        }
+    }
+
+    private void maybeUpdateServerModeInterfaceState(String iface, boolean available) {
+        if (available == mTetheredInterfaceWasAvailable || !iface.equals(mDefaultInterface)) return;
+
+        Log.d(TAG, (available ? "Tracking" : "No longer tracking")
+                + " interface in server mode: " + iface);
+
+        final int pendingCbs = mTetheredInterfaceRequests.beginBroadcast();
+        for (int i = 0; i < pendingCbs; i++) {
+            ITetheredInterfaceCallback item = mTetheredInterfaceRequests.getBroadcastItem(i);
+            if (available) {
+                notifyTetheredInterfaceAvailable(item, iface);
+            } else {
+                notifyTetheredInterfaceUnavailable(item);
+            }
+        }
+        mTetheredInterfaceRequests.finishBroadcast();
+        mTetheredInterfaceWasAvailable = available;
+    }
+
+    private void maybeTrackInterface(String iface) {
+        if (!iface.matches(mIfaceMatch)) {
+            return;
+        }
+
+        // If we don't already track this interface, and if this interface matches
+        // our regex, start tracking it.
+        if (mFactory.hasInterface(iface) || iface.equals(mDefaultInterface)) {
+            if (DBG) Log.w(TAG, "Ignoring already-tracked interface " + iface);
+            return;
+        }
+        if (DBG) Log.i(TAG, "maybeTrackInterface: " + iface);
+
+        // TODO: avoid making an interface default if it has configured NetworkCapabilities.
+        if (mDefaultInterface == null) {
+            mDefaultInterface = iface;
+        }
+
+        if (mIpConfigForDefaultInterface != null) {
+            updateIpConfiguration(iface, mIpConfigForDefaultInterface);
+            mIpConfigForDefaultInterface = null;
+        }
+
+        addInterface(iface);
+
+        broadcastInterfaceStateChange(iface);
+    }
+
+    private void trackAvailableInterfaces() {
+        try {
+            final String[] ifaces = mNetd.interfaceGetList();
+            for (String iface : ifaces) {
+                maybeTrackInterface(iface);
+            }
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "Could not get list of interfaces " + e);
+        }
+    }
+
+    private class InterfaceObserver extends BaseNetdUnsolicitedEventListener {
+
+        @Override
+        public void onInterfaceLinkStateChanged(String iface, boolean up) {
+            if (DBG) {
+                Log.i(TAG, "interfaceLinkStateChanged, iface: " + iface + ", up: " + up);
+            }
+            mHandler.post(() -> updateInterfaceState(iface, up));
+        }
+
+        @Override
+        public void onInterfaceAdded(String iface) {
+            if (DBG) {
+                Log.i(TAG, "onInterfaceAdded, iface: " + iface);
+            }
+            mHandler.post(() -> maybeTrackInterface(iface));
+        }
+
+        @Override
+        public void onInterfaceRemoved(String iface) {
+            if (DBG) {
+                Log.i(TAG, "onInterfaceRemoved, iface: " + iface);
+            }
+            mHandler.post(() -> stopTrackingInterface(iface));
+        }
+    }
+
+    private static class ListenerInfo {
+
+        boolean canUseRestrictedNetworks = false;
+
+        ListenerInfo(boolean canUseRestrictedNetworks) {
+            this.canUseRestrictedNetworks = canUseRestrictedNetworks;
+        }
+    }
+
+    /**
+     * Parses an Ethernet interface configuration
+     *
+     * @param configString represents an Ethernet configuration in the following format: {@code
+     * <interface name|mac address>;[Network Capabilities];[IP config];[Override Transport]}
+     */
+    private void parseEthernetConfig(String configString) {
+        final EthernetTrackerConfig config = createEthernetTrackerConfig(configString);
+        NetworkCapabilities nc = createNetworkCapabilities(
+                !TextUtils.isEmpty(config.mCapabilities)  /* clear default capabilities */,
+                config.mCapabilities, config.mTransport).build();
+        mNetworkCapabilities.put(config.mIface, nc);
+
+        if (null != config.mIpConfig) {
+            IpConfiguration ipConfig = parseStaticIpConfiguration(config.mIpConfig);
+            mIpConfigurations.put(config.mIface, ipConfig);
+        }
+    }
+
+    @VisibleForTesting
+    static EthernetTrackerConfig createEthernetTrackerConfig(@NonNull final String configString) {
+        Objects.requireNonNull(configString, "EthernetTrackerConfig requires non-null config");
+        return new EthernetTrackerConfig(configString.split(";", /* limit of tokens */ 4));
+    }
+
+    private static NetworkCapabilities createDefaultNetworkCapabilities(boolean isTestIface) {
+        NetworkCapabilities.Builder builder = createNetworkCapabilities(
+                false /* clear default capabilities */, null, null)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
+
+        if (isTestIface) {
+            builder.addTransportType(NetworkCapabilities.TRANSPORT_TEST);
+        } else {
+            builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+        }
+
+        return builder.build();
+    }
+
+    /**
+     * Parses a static list of network capabilities
+     *
+     * @param clearDefaultCapabilities Indicates whether or not to clear any default capabilities
+     * @param commaSeparatedCapabilities A comma separated string list of integer encoded
+     *                                   NetworkCapability.NET_CAPABILITY_* values
+     * @param overrideTransport A string representing a single integer encoded override transport
+     *                          type. Must be one of the NetworkCapability.TRANSPORT_*
+     *                          values. TRANSPORT_VPN is not supported. Errors with input
+     *                          will cause the override to be ignored.
+     */
+    @VisibleForTesting
+    static NetworkCapabilities.Builder createNetworkCapabilities(
+            boolean clearDefaultCapabilities, @Nullable String commaSeparatedCapabilities,
+            @Nullable String overrideTransport) {
+
+        final NetworkCapabilities.Builder builder = clearDefaultCapabilities
+                ? NetworkCapabilities.Builder.withoutDefaultCapabilities()
+                : new NetworkCapabilities.Builder();
+
+        // Determine the transport type. If someone has tried to define an override transport then
+        // attempt to add it. Since we can only have one override, all errors with it will
+        // gracefully default back to TRANSPORT_ETHERNET and warn the user. VPN is not allowed as an
+        // override type. Wifi Aware and LoWPAN are currently unsupported as well.
+        int transport = NetworkCapabilities.TRANSPORT_ETHERNET;
+        if (!TextUtils.isEmpty(overrideTransport)) {
+            try {
+                int parsedTransport = Integer.valueOf(overrideTransport);
+                if (parsedTransport == NetworkCapabilities.TRANSPORT_VPN
+                        || parsedTransport == NetworkCapabilities.TRANSPORT_WIFI_AWARE
+                        || parsedTransport == NetworkCapabilities.TRANSPORT_LOWPAN) {
+                    Log.e(TAG, "Override transport '" + parsedTransport + "' is not supported. "
+                            + "Defaulting to TRANSPORT_ETHERNET");
+                } else {
+                    transport = parsedTransport;
+                }
+            } catch (NumberFormatException nfe) {
+                Log.e(TAG, "Override transport type '" + overrideTransport + "' "
+                        + "could not be parsed. Defaulting to TRANSPORT_ETHERNET");
+            }
+        }
+
+        // Apply the transport. If the user supplied a valid number that is not a valid transport
+        // then adding will throw an exception. Default back to TRANSPORT_ETHERNET if that happens
+        try {
+            builder.addTransportType(transport);
+        } catch (IllegalArgumentException iae) {
+            Log.e(TAG, transport + " is not a valid NetworkCapability.TRANSPORT_* value. "
+                    + "Defaulting to TRANSPORT_ETHERNET");
+            builder.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET);
+        }
+
+        builder.setLinkUpstreamBandwidthKbps(100 * 1000);
+        builder.setLinkDownstreamBandwidthKbps(100 * 1000);
+
+        if (!TextUtils.isEmpty(commaSeparatedCapabilities)) {
+            for (String strNetworkCapability : commaSeparatedCapabilities.split(",")) {
+                if (!TextUtils.isEmpty(strNetworkCapability)) {
+                    try {
+                        builder.addCapability(Integer.valueOf(strNetworkCapability));
+                    } catch (NumberFormatException nfe) {
+                        Log.e(TAG, "Capability '" + strNetworkCapability + "' could not be parsed");
+                    } catch (IllegalArgumentException iae) {
+                        Log.e(TAG, strNetworkCapability + " is not a valid "
+                                + "NetworkCapability.NET_CAPABILITY_* value");
+                    }
+                }
+            }
+        }
+        // Ethernet networks have no way to update the following capabilities, so they always
+        // have them.
+        builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING);
+        builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED);
+        builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED);
+
+        return builder;
+    }
+
+    /**
+     * Parses static IP configuration.
+     *
+     * @param staticIpConfig represents static IP configuration in the following format: {@code
+     * ip=<ip-address/mask> gateway=<ip-address> dns=<comma-sep-ip-addresses>
+     *     domains=<comma-sep-domains>}
+     */
+    @VisibleForTesting
+    static IpConfiguration parseStaticIpConfiguration(String staticIpConfig) {
+        final StaticIpConfiguration.Builder staticIpConfigBuilder =
+                new StaticIpConfiguration.Builder();
+
+        for (String keyValueAsString : staticIpConfig.trim().split(" ")) {
+            if (TextUtils.isEmpty(keyValueAsString)) continue;
+
+            String[] pair = keyValueAsString.split("=");
+            if (pair.length != 2) {
+                throw new IllegalArgumentException("Unexpected token: " + keyValueAsString
+                        + " in " + staticIpConfig);
+            }
+
+            String key = pair[0];
+            String value = pair[1];
+
+            switch (key) {
+                case "ip":
+                    staticIpConfigBuilder.setIpAddress(new LinkAddress(value));
+                    break;
+                case "domains":
+                    staticIpConfigBuilder.setDomains(value);
+                    break;
+                case "gateway":
+                    staticIpConfigBuilder.setGateway(InetAddress.parseNumericAddress(value));
+                    break;
+                case "dns": {
+                    ArrayList<InetAddress> dnsAddresses = new ArrayList<>();
+                    for (String address: value.split(",")) {
+                        dnsAddresses.add(InetAddress.parseNumericAddress(address));
+                    }
+                    staticIpConfigBuilder.setDnsServers(dnsAddresses);
+                    break;
+                }
+                default : {
+                    throw new IllegalArgumentException("Unexpected key: " + key
+                            + " in " + staticIpConfig);
+                }
+            }
+        }
+        return createIpConfiguration(staticIpConfigBuilder.build());
+    }
+
+    private static IpConfiguration createIpConfiguration(
+            @NonNull final StaticIpConfiguration staticIpConfig) {
+        return new IpConfiguration.Builder().setStaticIpConfiguration(staticIpConfig).build();
+    }
+
+    private IpConfiguration getOrCreateIpConfiguration(String iface) {
+        IpConfiguration ret = mIpConfigurations.get(iface);
+        if (ret != null) return ret;
+        ret = new IpConfiguration();
+        ret.setIpAssignment(IpAssignment.DHCP);
+        ret.setProxySettings(ProxySettings.NONE);
+        return ret;
+    }
+
+    private void updateIfaceMatchRegexp() {
+        final String match = mDeps.getInterfaceRegexFromResource(mContext);
+        mIfaceMatch = mIncludeTestInterfaces
+                ? "(" + match + "|" + TEST_IFACE_REGEXP + ")"
+                : match;
+        Log.d(TAG, "Interface match regexp set to '" + mIfaceMatch + "'");
+    }
+
+    /**
+     * Validate if a given interface is valid for testing.
+     *
+     * @param iface the name of the interface to validate.
+     * @return {@code true} if test interfaces are enabled and the given {@code iface} has a test
+     * interface prefix, {@code false} otherwise.
+     */
+    public boolean isValidTestInterface(@NonNull final String iface) {
+        return mIncludeTestInterfaces && iface.matches(TEST_IFACE_REGEXP);
+    }
+
+    private void postAndWaitForRunnable(Runnable r) {
+        final ConditionVariable cv = new ConditionVariable();
+        if (mHandler.post(() -> {
+            r.run();
+            cv.open();
+        })) {
+            cv.block(2000L);
+        }
+    }
+
+    @VisibleForTesting(visibility = PACKAGE)
+    protected void setEthernetEnabled(boolean enabled) {
+        mHandler.post(() -> {
+            int newState = enabled ? ETHERNET_STATE_ENABLED : ETHERNET_STATE_DISABLED;
+            if (mEthernetState == newState) return;
+
+            mEthernetState = newState;
+
+            if (enabled) {
+                trackAvailableInterfaces();
+            } else {
+                // TODO: maybe also disable server mode interface as well.
+                untrackFactoryInterfaces();
+            }
+            broadcastEthernetStateChange(mEthernetState);
+        });
+    }
+
+    private void untrackFactoryInterfaces() {
+        for (String iface : mFactory.getAvailableInterfaces(true /* includeRestricted */)) {
+            stopTrackingInterface(iface);
+        }
+    }
+
+    private void unicastEthernetStateChange(@NonNull IEthernetServiceListener listener,
+            int state) {
+        ensureRunningOnEthernetServiceThread();
+        try {
+            listener.onEthernetStateChanged(state);
+        } catch (RemoteException e) {
+            // Do nothing here.
+        }
+    }
+
+    private void broadcastEthernetStateChange(int state) {
+        ensureRunningOnEthernetServiceThread();
+        final int n = mListeners.beginBroadcast();
+        for (int i = 0; i < n; i++) {
+            try {
+                mListeners.getBroadcastItem(i).onEthernetStateChanged(state);
+            } catch (RemoteException e) {
+                // Do nothing here.
+            }
+        }
+        mListeners.finishBroadcast();
+    }
+
+    void dump(FileDescriptor fd, IndentingPrintWriter pw, String[] args) {
+        postAndWaitForRunnable(() -> {
+            pw.println(getClass().getSimpleName());
+            pw.println("Ethernet interface name filter: " + mIfaceMatch);
+            pw.println("Default interface: " + mDefaultInterface);
+            pw.println("Default interface mode: " + mDefaultInterfaceMode);
+            pw.println("Tethered interface requests: "
+                    + mTetheredInterfaceRequests.getRegisteredCallbackCount());
+            pw.println("Listeners: " + mListeners.getRegisteredCallbackCount());
+            pw.println("IP Configurations:");
+            pw.increaseIndent();
+            for (String iface : mIpConfigurations.keySet()) {
+                pw.println(iface + ": " + mIpConfigurations.get(iface));
+            }
+            pw.decreaseIndent();
+            pw.println();
+
+            pw.println("Network Capabilities:");
+            pw.increaseIndent();
+            for (String iface : mNetworkCapabilities.keySet()) {
+                pw.println(iface + ": " + mNetworkCapabilities.get(iface));
+            }
+            pw.decreaseIndent();
+            pw.println();
+
+            mFactory.dump(fd, pw, args);
+        });
+    }
+
+    @VisibleForTesting
+    static class EthernetTrackerConfig {
+        final String mIface;
+        final String mCapabilities;
+        final String mIpConfig;
+        final String mTransport;
+
+        EthernetTrackerConfig(@NonNull final String[] tokens) {
+            Objects.requireNonNull(tokens, "EthernetTrackerConfig requires non-null tokens");
+            mIface = tokens[0];
+            mCapabilities = tokens.length > 1 ? tokens[1] : null;
+            mIpConfig = tokens.length > 2 && !TextUtils.isEmpty(tokens[2]) ? tokens[2] : null;
+            mTransport = tokens.length > 3 ? tokens[3] : null;
+        }
+    }
+}
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/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 0528f29..d79bdb8 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() {
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-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..7bb7cb5 100644
--- a/tests/common/Android.bp
+++ b/tests/common/Android.bp
@@ -43,10 +43,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 +67,11 @@
         "FrameworksNetTestsLib",
         "NetdStaticLibTestsLib",
         "NetworkStaticLibTestsLib",
+        "NetworkStackTestsLib",
+        "TetheringTestsLatestSdkLib",
+        "TetheringIntegrationTestsLatestSdkLib",
     ],
-    jarjar_rules: ":connectivity-jarjar-rules",
+    jarjar_rules: ":ConnectivityCoverageJarJarRules",
     manifest: "AndroidManifest_coverage.xml",
     visibility: ["//visibility:private"],
 }
@@ -80,7 +96,6 @@
         "mockito-target-extended-minus-junit4",
         "modules-utils-native-coverage-listener",
         "ConnectivityCoverageTestsLib",
-        "TetheringCoverageTestsLib",
     ],
     jni_libs: [
         // For mockito extended
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/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
index a840242..f460180 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
@@ -149,7 +149,7 @@
 
     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;
 
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..b6218d2 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
@@ -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 {
diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
index 9fa146f..0c4c370 100644
--- a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
+++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
@@ -20,14 +20,18 @@
 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.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,6 +50,7 @@
 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;
@@ -55,8 +60,10 @@
 import com.android.internal.util.HexDump;
 import com.android.networkstack.apishim.Ikev2VpnProfileBuilderShimImpl;
 import com.android.networkstack.apishim.Ikev2VpnProfileShimImpl;
+import com.android.networkstack.apishim.common.Ikev2VpnProfileBuilderShim;
 import com.android.networkstack.apishim.common.Ikev2VpnProfileShim;
 import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
+import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.RecorderCallback.CallbackEntry;
@@ -64,6 +71,7 @@
 
 import org.bouncycastle.x509.X509V1CertificateGenerator;
 import org.junit.After;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -85,7 +93,8 @@
 @AppModeFull(reason = "Appops state changes disallowed for instant apps (OP_ACTIVATE_PLATFORM_VPN)")
 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"
@@ -204,51 +213,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 +292,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 */);
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/native/Android.bp b/tests/native/Android.bp
new file mode 100644
index 0000000..9c286d8
--- /dev/null
+++ b/tests/native/Android.bp
@@ -0,0 +1,34 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_test {
+    name: "connectivity_native_test",
+    test_suites: [
+        "general-tests",
+        "mts-tethering",
+        "vts",
+    ],
+    min_sdk_version: "31",
+    require_root: true,
+    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",
+    defaults: ["connectivity-mainline-presubmit-cc-defaults"],
+}
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..8b089ab
--- /dev/null
+++ b/tests/native/connectivity_native_test.cpp
@@ -0,0 +1,281 @@
+/*
+ * 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 <android-modules-utils/sdk_level.h>
+#include <cutils/misc.h>  // FIRST_APPLICATION_UID
+#include <gtest/gtest.h>
+#include <netinet/in.h>
+#include <android/binder_manager.h>
+#include <android/binder_process.h>
+
+#include <aidl/android/net/connectivity/aidl/ConnectivityNative.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.";
+
+        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..4c63cba 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -76,6 +76,7 @@
         "java/com/android/server/IpSecServiceParameterizedTest.java",
         "java/com/android/server/IpSecServiceRefcountedResourceTest.java",
         "java/com/android/server/IpSecServiceTest.java",
+        "java/com/android/server/NativeDaemonConnectorTest.java",
         "java/com/android/server/NetworkManagementServiceTest.java",
         "java/com/android/server/NsdServiceTest.java",
         "java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java",
@@ -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/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/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/ethernet/EthernetNetworkFactoryTest.java b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
new file mode 100644
index 0000000..dfb4fcc
--- /dev/null
+++ b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
@@ -0,0 +1,781 @@
+/*
+ * 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.server.ethernet;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+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;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.app.test.MockAnswerUtil.AnswerWithArguments;
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.ConnectivityManager;
+import android.net.EthernetNetworkManagementException;
+import android.net.EthernetNetworkSpecifier;
+import android.net.INetworkInterfaceOutcomeReceiver;
+import android.net.IpConfiguration;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkProvider;
+import android.net.NetworkRequest;
+import android.net.StaticIpConfiguration;
+import android.net.ip.IpClientCallbacks;
+import android.net.ip.IpClientManager;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.test.TestLooper;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.InterfaceParams;
+import com.android.testutils.DevSdkIgnoreRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class EthernetNetworkFactoryTest {
+    private static final int TIMEOUT_MS = 2_000;
+    private static final String TEST_IFACE = "test123";
+    private static final INetworkInterfaceOutcomeReceiver NULL_LISTENER = null;
+    private static final String IP_ADDR = "192.0.2.2/25";
+    private static final LinkAddress LINK_ADDR = new LinkAddress(IP_ADDR);
+    private static final String HW_ADDR = "01:02:03:04:05:06";
+    private TestLooper mLooper;
+    private Handler mHandler;
+    private EthernetNetworkFactory mNetFactory = null;
+    private IpClientCallbacks mIpClientCallbacks;
+    @Mock private Context mContext;
+    @Mock private Resources mResources;
+    @Mock private EthernetNetworkFactory.Dependencies mDeps;
+    @Mock private IpClientManager mIpClient;
+    @Mock private EthernetNetworkAgent mNetworkAgent;
+    @Mock private InterfaceParams mInterfaceParams;
+    @Mock private Network mMockNetwork;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        setupNetworkAgentMock();
+        setupIpClientMock();
+        setupContext();
+    }
+
+    //TODO: Move away from usage of TestLooper in order to move this logic back into @Before.
+    private void initEthernetNetworkFactory() {
+        mLooper = new TestLooper();
+        mHandler = new Handler(mLooper.getLooper());
+        mNetFactory = new EthernetNetworkFactory(mHandler, mContext, mDeps);
+    }
+
+    private void setupNetworkAgentMock() {
+        when(mDeps.makeEthernetNetworkAgent(any(), any(), any(), any(), any(), any(), any()))
+                .thenAnswer(new AnswerWithArguments() {
+                                       public EthernetNetworkAgent answer(
+                                               Context context,
+                                               Looper looper,
+                                               NetworkCapabilities nc,
+                                               LinkProperties lp,
+                                               NetworkAgentConfig config,
+                                               NetworkProvider provider,
+                                               EthernetNetworkAgent.Callbacks cb) {
+                                           when(mNetworkAgent.getCallbacks()).thenReturn(cb);
+                                           when(mNetworkAgent.getNetwork())
+                                                   .thenReturn(mMockNetwork);
+                                           return mNetworkAgent;
+                                       }
+                                   }
+        );
+    }
+
+    private void setupIpClientMock() throws Exception {
+        doAnswer(inv -> {
+            // these tests only support one concurrent IpClient, so make sure we do not accidentally
+            // create a mess.
+            assertNull("An IpClient has already been created.", mIpClientCallbacks);
+
+            mIpClientCallbacks = inv.getArgument(2);
+            mIpClientCallbacks.onIpClientCreated(null);
+            mLooper.dispatchAll();
+            return null;
+        }).when(mDeps).makeIpClient(any(Context.class), anyString(), any());
+
+        doAnswer(inv -> {
+            mIpClientCallbacks.onQuit();
+            mLooper.dispatchAll();
+            mIpClientCallbacks = null;
+            return null;
+        }).when(mIpClient).shutdown();
+
+        when(mDeps.makeIpClientManager(any())).thenReturn(mIpClient);
+    }
+
+    private void triggerOnProvisioningSuccess() {
+        mIpClientCallbacks.onProvisioningSuccess(new LinkProperties());
+        mLooper.dispatchAll();
+    }
+
+    private void triggerOnProvisioningFailure() {
+        mIpClientCallbacks.onProvisioningFailure(new LinkProperties());
+        mLooper.dispatchAll();
+    }
+
+    private void triggerOnReachabilityLost() {
+        mIpClientCallbacks.onReachabilityLost("ReachabilityLost");
+        mLooper.dispatchAll();
+    }
+
+    private void setupContext() {
+        when(mDeps.getTcpBufferSizesFromResource(eq(mContext))).thenReturn("");
+    }
+
+    @After
+    public void tearDown() {
+        // looper is shared with the network agents, so there may still be messages to dispatch on
+        // tear down.
+        mLooper.dispatchAll();
+    }
+
+    private NetworkCapabilities createDefaultFilterCaps() {
+        return NetworkCapabilities.Builder.withoutDefaultCapabilities()
+                .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                .build();
+    }
+
+    private NetworkCapabilities.Builder createInterfaceCapsBuilder(final int transportType) {
+        return new NetworkCapabilities.Builder()
+                .addTransportType(transportType)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
+    }
+
+    private NetworkRequest.Builder createDefaultRequestBuilder() {
+        return new NetworkRequest.Builder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+    }
+
+    private NetworkRequest createDefaultRequest() {
+        return createDefaultRequestBuilder().build();
+    }
+
+    private IpConfiguration createDefaultIpConfig() {
+        IpConfiguration ipConfig = new IpConfiguration();
+        ipConfig.setIpAssignment(IpConfiguration.IpAssignment.DHCP);
+        ipConfig.setProxySettings(IpConfiguration.ProxySettings.NONE);
+        return ipConfig;
+    }
+
+    /**
+     * Create an {@link IpConfiguration} with an associated {@link StaticIpConfiguration}.
+     *
+     * @return {@link IpConfiguration} with its {@link StaticIpConfiguration} set.
+     */
+    private IpConfiguration createStaticIpConfig() {
+        final IpConfiguration ipConfig = new IpConfiguration();
+        ipConfig.setIpAssignment(IpConfiguration.IpAssignment.STATIC);
+        ipConfig.setStaticIpConfiguration(
+                new StaticIpConfiguration.Builder().setIpAddress(LINK_ADDR).build());
+        return ipConfig;
+    }
+
+    // creates an interface with provisioning in progress (since updating the interface link state
+    // automatically starts the provisioning process)
+    private void createInterfaceUndergoingProvisioning(String iface) {
+        // Default to the ethernet transport type.
+        createInterfaceUndergoingProvisioning(iface, NetworkCapabilities.TRANSPORT_ETHERNET);
+    }
+
+    private void createInterfaceUndergoingProvisioning(
+            @NonNull final String iface, final int transportType) {
+        final IpConfiguration ipConfig = createDefaultIpConfig();
+        mNetFactory.addInterface(iface, HW_ADDR, ipConfig,
+                createInterfaceCapsBuilder(transportType).build());
+        assertTrue(mNetFactory.updateInterfaceLinkState(iface, true, NULL_LISTENER));
+        verifyStart(ipConfig);
+        clearInvocations(mDeps);
+        clearInvocations(mIpClient);
+    }
+
+    // creates a provisioned interface
+    private void createAndVerifyProvisionedInterface(String iface) throws Exception {
+        // Default to the ethernet transport type.
+        createAndVerifyProvisionedInterface(iface, NetworkCapabilities.TRANSPORT_ETHERNET,
+                ConnectivityManager.TYPE_ETHERNET);
+    }
+
+    private void createVerifyAndRemoveProvisionedInterface(final int transportType,
+            final int expectedLegacyType) throws Exception {
+        createAndVerifyProvisionedInterface(TEST_IFACE, transportType,
+                expectedLegacyType);
+        mNetFactory.removeInterface(TEST_IFACE);
+    }
+
+    private void createAndVerifyProvisionedInterface(
+            @NonNull final String iface, final int transportType, final int expectedLegacyType)
+            throws Exception {
+        createInterfaceUndergoingProvisioning(iface, transportType);
+        triggerOnProvisioningSuccess();
+        // provisioning succeeded, verify that the network agent is created, registered, marked
+        // as connected and legacy type are correctly set.
+        final ArgumentCaptor<NetworkCapabilities> ncCaptor = ArgumentCaptor.forClass(
+                NetworkCapabilities.class);
+        verify(mDeps).makeEthernetNetworkAgent(any(), any(), ncCaptor.capture(), any(),
+                argThat(x -> x.getLegacyType() == expectedLegacyType), any(), any());
+        assertEquals(
+                new EthernetNetworkSpecifier(iface), ncCaptor.getValue().getNetworkSpecifier());
+        verifyNetworkAgentRegistersAndConnects();
+        clearInvocations(mDeps);
+        clearInvocations(mNetworkAgent);
+    }
+
+    // creates an unprovisioned interface
+    private void createUnprovisionedInterface(String iface) throws Exception {
+        // To create an unprovisioned interface, provision and then "stop" it, i.e. stop its
+        // NetworkAgent and IpClient. One way this can be done is by provisioning an interface and
+        // then calling onNetworkUnwanted.
+        createAndVerifyProvisionedInterface(iface);
+
+        mNetworkAgent.getCallbacks().onNetworkUnwanted();
+        mLooper.dispatchAll();
+        verifyStop();
+
+        clearInvocations(mIpClient);
+        clearInvocations(mNetworkAgent);
+    }
+
+    @Test
+    public void testAcceptRequest() throws Exception {
+        initEthernetNetworkFactory();
+        createInterfaceUndergoingProvisioning(TEST_IFACE);
+        assertTrue(mNetFactory.acceptRequest(createDefaultRequest()));
+
+        NetworkRequest wifiRequest = createDefaultRequestBuilder()
+                .removeTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI).build();
+        assertFalse(mNetFactory.acceptRequest(wifiRequest));
+    }
+
+    @Test
+    public void testUpdateInterfaceLinkStateForActiveProvisioningInterface() throws Exception {
+        initEthernetNetworkFactory();
+        createInterfaceUndergoingProvisioning(TEST_IFACE);
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        // verify that the IpClient gets shut down when interface state changes to down.
+        final boolean ret =
+                mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */, listener);
+
+        assertTrue(ret);
+        verify(mIpClient).shutdown();
+        assertEquals(listener.expectOnResult(), TEST_IFACE);
+    }
+
+    @Test
+    public void testUpdateInterfaceLinkStateForProvisionedInterface() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        final boolean ret =
+                mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */, listener);
+
+        assertTrue(ret);
+        verifyStop();
+        assertEquals(listener.expectOnResult(), TEST_IFACE);
+    }
+
+    @Test
+    public void testUpdateInterfaceLinkStateForUnprovisionedInterface() throws Exception {
+        initEthernetNetworkFactory();
+        createUnprovisionedInterface(TEST_IFACE);
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        final boolean ret =
+                mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */, listener);
+
+        assertTrue(ret);
+        // There should not be an active IPClient or NetworkAgent.
+        verify(mDeps, never()).makeIpClient(any(), any(), any());
+        verify(mDeps, never())
+                .makeEthernetNetworkAgent(any(), any(), any(), any(), any(), any(), any());
+        assertEquals(listener.expectOnResult(), TEST_IFACE);
+    }
+
+    @Test
+    public void testUpdateInterfaceLinkStateForNonExistingInterface() throws Exception {
+        initEthernetNetworkFactory();
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        // if interface was never added, link state cannot be updated.
+        final boolean ret =
+                mNetFactory.updateInterfaceLinkState(TEST_IFACE, true /* up */, listener);
+
+        assertFalse(ret);
+        verifyNoStopOrStart();
+        listener.expectOnError();
+    }
+
+    @Test
+    public void testUpdateInterfaceLinkStateWithNoChanges() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        final boolean ret =
+                mNetFactory.updateInterfaceLinkState(TEST_IFACE, true /* up */, listener);
+
+        assertFalse(ret);
+        verifyNoStopOrStart();
+        listener.expectOnError();
+    }
+
+    @Test
+    public void testNeedNetworkForOnProvisionedInterface() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+        mNetFactory.needNetworkFor(createDefaultRequest());
+        verify(mIpClient, never()).startProvisioning(any());
+    }
+
+    @Test
+    public void testNeedNetworkForOnUnprovisionedInterface() throws Exception {
+        initEthernetNetworkFactory();
+        createUnprovisionedInterface(TEST_IFACE);
+        mNetFactory.needNetworkFor(createDefaultRequest());
+        verify(mIpClient).startProvisioning(any());
+
+        triggerOnProvisioningSuccess();
+        verifyNetworkAgentRegistersAndConnects();
+    }
+
+    @Test
+    public void testNeedNetworkForOnInterfaceUndergoingProvisioning() throws Exception {
+        initEthernetNetworkFactory();
+        createInterfaceUndergoingProvisioning(TEST_IFACE);
+        mNetFactory.needNetworkFor(createDefaultRequest());
+        verify(mIpClient, never()).startProvisioning(any());
+
+        triggerOnProvisioningSuccess();
+        verifyNetworkAgentRegistersAndConnects();
+    }
+
+    @Test
+    public void testProvisioningLoss() throws Exception {
+        initEthernetNetworkFactory();
+        when(mDeps.getNetworkInterfaceByName(TEST_IFACE)).thenReturn(mInterfaceParams);
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+
+        triggerOnProvisioningFailure();
+        verifyStop();
+        // provisioning loss should trigger a retry, since the interface is still there
+        verify(mIpClient).startProvisioning(any());
+    }
+
+    @Test
+    public void testProvisioningLossForDisappearedInterface() throws Exception {
+        initEthernetNetworkFactory();
+        // mocked method returns null by default, but just to be explicit in the test:
+        when(mDeps.getNetworkInterfaceByName(eq(TEST_IFACE))).thenReturn(null);
+
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+        triggerOnProvisioningFailure();
+
+        // the interface disappeared and getNetworkInterfaceByName returns null, we should not retry
+        verify(mIpClient, never()).startProvisioning(any());
+        verifyNoStopOrStart();
+    }
+
+    private void verifyNoStopOrStart() {
+        verify(mNetworkAgent, never()).register();
+        verify(mIpClient, never()).shutdown();
+        verify(mNetworkAgent, never()).unregister();
+        verify(mIpClient, never()).startProvisioning(any());
+    }
+
+    @Test
+    public void testIpClientIsNotStartedWhenLinkIsDown() throws Exception {
+        initEthernetNetworkFactory();
+        createUnprovisionedInterface(TEST_IFACE);
+        mNetFactory.updateInterfaceLinkState(TEST_IFACE, false, NULL_LISTENER);
+
+        mNetFactory.needNetworkFor(createDefaultRequest());
+
+        verify(mDeps, never()).makeIpClient(any(), any(), any());
+
+        // BUG(b/191854824): requesting a network with a specifier (Android Auto use case) should
+        // not start an IpClient when the link is down, but fixing this may make matters worse by
+        // tiggering b/197548738.
+        NetworkRequest specificNetRequest = new NetworkRequest.Builder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                .setNetworkSpecifier(new EthernetNetworkSpecifier(TEST_IFACE))
+                .build();
+        mNetFactory.needNetworkFor(specificNetRequest);
+        mNetFactory.releaseNetworkFor(specificNetRequest);
+
+        mNetFactory.updateInterfaceLinkState(TEST_IFACE, true, NULL_LISTENER);
+        // TODO: change to once when b/191854824 is fixed.
+        verify(mDeps, times(2)).makeIpClient(any(), eq(TEST_IFACE), any());
+    }
+
+    @Test
+    public void testLinkPropertiesChanged() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+
+        LinkProperties lp = new LinkProperties();
+        mIpClientCallbacks.onLinkPropertiesChange(lp);
+        mLooper.dispatchAll();
+        verify(mNetworkAgent).sendLinkPropertiesImpl(same(lp));
+    }
+
+    @Test
+    public void testNetworkUnwanted() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+
+        mNetworkAgent.getCallbacks().onNetworkUnwanted();
+        mLooper.dispatchAll();
+        verifyStop();
+    }
+
+    @Test
+    public void testNetworkUnwantedWithStaleNetworkAgent() throws Exception {
+        initEthernetNetworkFactory();
+        // ensures provisioning is restarted after provisioning loss
+        when(mDeps.getNetworkInterfaceByName(TEST_IFACE)).thenReturn(mInterfaceParams);
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+
+        EthernetNetworkAgent.Callbacks oldCbs = mNetworkAgent.getCallbacks();
+        // replace network agent in EthernetNetworkFactory
+        // Loss of provisioning will restart the ip client and network agent.
+        triggerOnProvisioningFailure();
+        verify(mDeps).makeIpClient(any(), any(), any());
+
+        triggerOnProvisioningSuccess();
+        verify(mDeps).makeEthernetNetworkAgent(any(), any(), any(), any(), any(), any(), any());
+
+        // verify that unwanted is ignored
+        clearInvocations(mIpClient);
+        clearInvocations(mNetworkAgent);
+        oldCbs.onNetworkUnwanted();
+        verify(mIpClient, never()).shutdown();
+        verify(mNetworkAgent, never()).unregister();
+    }
+
+    @Test
+    public void testTransportOverrideIsCorrectlySet() throws Exception {
+        initEthernetNetworkFactory();
+        // createProvisionedInterface() has verifications in place for transport override
+        // functionality which for EthernetNetworkFactory is network score and legacy type mappings.
+        createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_ETHERNET,
+                ConnectivityManager.TYPE_ETHERNET);
+        createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_BLUETOOTH,
+                ConnectivityManager.TYPE_BLUETOOTH);
+        createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_WIFI,
+                ConnectivityManager.TYPE_WIFI);
+        createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_CELLULAR,
+                ConnectivityManager.TYPE_MOBILE);
+        createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_LOWPAN,
+                ConnectivityManager.TYPE_NONE);
+        createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_WIFI_AWARE,
+                ConnectivityManager.TYPE_NONE);
+        createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_TEST,
+                ConnectivityManager.TYPE_NONE);
+    }
+
+    @Test
+    public void testReachabilityLoss() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+
+        triggerOnReachabilityLost();
+
+        // Reachability loss should trigger a stop and start, since the interface is still there
+        verifyRestart(createDefaultIpConfig());
+    }
+
+    private IpClientCallbacks getStaleIpClientCallbacks() throws Exception {
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+        final IpClientCallbacks staleIpClientCallbacks = mIpClientCallbacks;
+        mNetFactory.removeInterface(TEST_IFACE);
+        verifyStop();
+        assertNotSame(mIpClientCallbacks, staleIpClientCallbacks);
+        return staleIpClientCallbacks;
+    }
+
+    @Test
+    public void testIgnoreOnIpLayerStartedCallbackForStaleCallback() throws Exception {
+        initEthernetNetworkFactory();
+        final IpClientCallbacks staleIpClientCallbacks = getStaleIpClientCallbacks();
+
+        staleIpClientCallbacks.onProvisioningSuccess(new LinkProperties());
+        mLooper.dispatchAll();
+
+        verify(mIpClient, never()).startProvisioning(any());
+        verify(mNetworkAgent, never()).register();
+    }
+
+    @Test
+    public void testIgnoreOnIpLayerStoppedCallbackForStaleCallback() throws Exception {
+        initEthernetNetworkFactory();
+        when(mDeps.getNetworkInterfaceByName(TEST_IFACE)).thenReturn(mInterfaceParams);
+        final IpClientCallbacks staleIpClientCallbacks = getStaleIpClientCallbacks();
+
+        staleIpClientCallbacks.onProvisioningFailure(new LinkProperties());
+        mLooper.dispatchAll();
+
+        verify(mIpClient, never()).startProvisioning(any());
+    }
+
+    @Test
+    public void testIgnoreLinkPropertiesCallbackForStaleCallback() throws Exception {
+        initEthernetNetworkFactory();
+        final IpClientCallbacks staleIpClientCallbacks = getStaleIpClientCallbacks();
+        final LinkProperties lp = new LinkProperties();
+
+        staleIpClientCallbacks.onLinkPropertiesChange(lp);
+        mLooper.dispatchAll();
+
+        verify(mNetworkAgent, never()).sendLinkPropertiesImpl(eq(lp));
+    }
+
+    @Test
+    public void testIgnoreNeighborLossCallbackForStaleCallback() throws Exception {
+        initEthernetNetworkFactory();
+        final IpClientCallbacks staleIpClientCallbacks = getStaleIpClientCallbacks();
+
+        staleIpClientCallbacks.onReachabilityLost("Neighbor Lost");
+        mLooper.dispatchAll();
+
+        verify(mIpClient, never()).startProvisioning(any());
+        verify(mNetworkAgent, never()).register();
+    }
+
+    private void verifyRestart(@NonNull final IpConfiguration ipConfig) {
+        verifyStop();
+        verifyStart(ipConfig);
+    }
+
+    private void verifyStart(@NonNull final IpConfiguration ipConfig) {
+        verify(mDeps).makeIpClient(any(Context.class), anyString(), any());
+        verify(mIpClient).startProvisioning(
+                argThat(x -> Objects.equals(x.mStaticIpConfig, ipConfig.getStaticIpConfiguration()))
+        );
+    }
+
+    private void verifyStop() {
+        verify(mIpClient).shutdown();
+        verify(mNetworkAgent).unregister();
+    }
+
+    private void verifyNetworkAgentRegistersAndConnects() {
+        verify(mNetworkAgent).register();
+        verify(mNetworkAgent).markConnected();
+    }
+
+    private static final class TestNetworkManagementListener
+            implements INetworkInterfaceOutcomeReceiver {
+        private final CompletableFuture<String> mResult = new CompletableFuture<>();
+
+        @Override
+        public void onResult(@NonNull String iface) {
+            mResult.complete(iface);
+        }
+
+        @Override
+        public void onError(@NonNull EthernetNetworkManagementException exception) {
+            mResult.completeExceptionally(exception);
+        }
+
+        String expectOnResult() throws Exception {
+            return mResult.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        }
+
+        void expectOnError() throws Exception {
+            assertThrows(EthernetNetworkManagementException.class, () -> {
+                try {
+                    mResult.get();
+                } catch (ExecutionException e) {
+                    throw e.getCause();
+                }
+            });
+        }
+
+        @Override
+        public IBinder asBinder() {
+            return null;
+        }
+    }
+
+    @Test
+    public void testUpdateInterfaceCallsListenerCorrectlyOnSuccess() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+        final NetworkCapabilities capabilities = createDefaultFilterCaps();
+        final IpConfiguration ipConfiguration = createStaticIpConfig();
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        mNetFactory.updateInterface(TEST_IFACE, ipConfiguration, capabilities, listener);
+        triggerOnProvisioningSuccess();
+
+        assertEquals(listener.expectOnResult(), TEST_IFACE);
+    }
+
+    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+    @Test
+    public void testUpdateInterfaceAbortsOnConcurrentRemoveInterface() throws Exception {
+        initEthernetNetworkFactory();
+        verifyNetworkManagementCallIsAbortedWhenInterrupted(
+                TEST_IFACE,
+                () -> mNetFactory.removeInterface(TEST_IFACE));
+    }
+
+    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+    @Test
+    public void testUpdateInterfaceAbortsOnConcurrentUpdateInterfaceLinkState() throws Exception {
+        initEthernetNetworkFactory();
+        verifyNetworkManagementCallIsAbortedWhenInterrupted(
+                TEST_IFACE,
+                () -> mNetFactory.updateInterfaceLinkState(TEST_IFACE, false, NULL_LISTENER));
+    }
+
+    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+    @Test
+    public void testUpdateInterfaceCallsListenerCorrectlyOnConcurrentRequests() throws Exception {
+        initEthernetNetworkFactory();
+        final NetworkCapabilities capabilities = createDefaultFilterCaps();
+        final IpConfiguration ipConfiguration = createStaticIpConfig();
+        final TestNetworkManagementListener successfulListener =
+                new TestNetworkManagementListener();
+
+        // If two calls come in before the first one completes, the first listener will be aborted
+        // and the second one will be successful.
+        verifyNetworkManagementCallIsAbortedWhenInterrupted(
+                TEST_IFACE,
+                () -> {
+                    mNetFactory.updateInterface(
+                            TEST_IFACE, ipConfiguration, capabilities, successfulListener);
+                    triggerOnProvisioningSuccess();
+                });
+
+        assertEquals(successfulListener.expectOnResult(), TEST_IFACE);
+    }
+
+    private void verifyNetworkManagementCallIsAbortedWhenInterrupted(
+            @NonNull final String iface,
+            @NonNull final Runnable interruptingRunnable) throws Exception {
+        createAndVerifyProvisionedInterface(iface);
+        final NetworkCapabilities capabilities = createDefaultFilterCaps();
+        final IpConfiguration ipConfiguration = createStaticIpConfig();
+        final TestNetworkManagementListener failedListener = new TestNetworkManagementListener();
+
+        // An active update request will be aborted on interrupt prior to provisioning completion.
+        mNetFactory.updateInterface(iface, ipConfiguration, capabilities, failedListener);
+        interruptingRunnable.run();
+
+        failedListener.expectOnError();
+    }
+
+    @Test
+    public void testUpdateInterfaceRestartsAgentCorrectly() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+        final NetworkCapabilities capabilities = createDefaultFilterCaps();
+        final IpConfiguration ipConfiguration = createStaticIpConfig();
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        mNetFactory.updateInterface(TEST_IFACE, ipConfiguration, capabilities, listener);
+        triggerOnProvisioningSuccess();
+
+        assertEquals(listener.expectOnResult(), TEST_IFACE);
+        verify(mDeps).makeEthernetNetworkAgent(any(), any(),
+                eq(capabilities), any(), any(), any(), any());
+        verifyRestart(ipConfiguration);
+    }
+
+    @Test
+    public void testUpdateInterfaceForNonExistingInterface() throws Exception {
+        initEthernetNetworkFactory();
+        // No interface exists due to not calling createAndVerifyProvisionedInterface(...).
+        final NetworkCapabilities capabilities = createDefaultFilterCaps();
+        final IpConfiguration ipConfiguration = createStaticIpConfig();
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        mNetFactory.updateInterface(TEST_IFACE, ipConfiguration, capabilities, listener);
+
+        verifyNoStopOrStart();
+        listener.expectOnError();
+    }
+
+    @Test
+    public void testUpdateInterfaceWithNullIpConfiguration() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+
+        final IpConfiguration initialIpConfig = createStaticIpConfig();
+        mNetFactory.updateInterface(TEST_IFACE, initialIpConfig, null /*capabilities*/,
+                null /*listener*/);
+        triggerOnProvisioningSuccess();
+        verifyRestart(initialIpConfig);
+
+        // TODO: have verifyXyz functions clear invocations.
+        clearInvocations(mDeps);
+        clearInvocations(mIpClient);
+        clearInvocations(mNetworkAgent);
+
+
+        // verify that sending a null ipConfig does not update the current ipConfig.
+        mNetFactory.updateInterface(TEST_IFACE, null /*ipConfig*/, null /*capabilities*/,
+                null /*listener*/);
+        triggerOnProvisioningSuccess();
+        verifyRestart(initialIpConfig);
+    }
+}
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java b/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java
new file mode 100644
index 0000000..dd1f1ed
--- /dev/null
+++ b/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java
@@ -0,0 +1,372 @@
+/*
+ * 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.ethernet;
+
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.INetworkInterfaceOutcomeReceiver;
+import android.net.EthernetNetworkUpdateRequest;
+import android.net.IpConfiguration;
+import android.net.NetworkCapabilities;
+import android.os.Handler;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class EthernetServiceImplTest {
+    private static final String TEST_IFACE = "test123";
+    private static final EthernetNetworkUpdateRequest UPDATE_REQUEST =
+            new EthernetNetworkUpdateRequest.Builder()
+                    .setIpConfiguration(new IpConfiguration())
+                    .setNetworkCapabilities(new NetworkCapabilities.Builder().build())
+                    .build();
+    private static final EthernetNetworkUpdateRequest UPDATE_REQUEST_WITHOUT_CAPABILITIES =
+            new EthernetNetworkUpdateRequest.Builder()
+                    .setIpConfiguration(new IpConfiguration())
+                    .build();
+    private static final EthernetNetworkUpdateRequest UPDATE_REQUEST_WITHOUT_IP_CONFIG =
+            new EthernetNetworkUpdateRequest.Builder()
+                    .setNetworkCapabilities(new NetworkCapabilities.Builder().build())
+                    .build();
+    private static final INetworkInterfaceOutcomeReceiver NULL_LISTENER = null;
+    private EthernetServiceImpl mEthernetServiceImpl;
+    @Mock private Context mContext;
+    @Mock private Handler mHandler;
+    @Mock private EthernetTracker mEthernetTracker;
+    @Mock private PackageManager mPackageManager;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        doReturn(mPackageManager).when(mContext).getPackageManager();
+        mEthernetServiceImpl = new EthernetServiceImpl(mContext, mHandler, mEthernetTracker);
+        mEthernetServiceImpl.mStarted.set(true);
+        toggleAutomotiveFeature(true);
+        shouldTrackIface(TEST_IFACE, true);
+    }
+
+    private void toggleAutomotiveFeature(final boolean isEnabled) {
+        doReturn(isEnabled)
+                .when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
+    }
+
+    private void shouldTrackIface(@NonNull final String iface, final boolean shouldTrack) {
+        doReturn(shouldTrack).when(mEthernetTracker).isTrackingInterface(iface);
+    }
+
+    @Test
+    public void testSetConfigurationRejectsWhenEthNotStarted() {
+        mEthernetServiceImpl.mStarted.set(false);
+        assertThrows(IllegalStateException.class, () -> {
+            mEthernetServiceImpl.setConfiguration("" /* iface */, new IpConfiguration());
+        });
+    }
+
+    @Test
+    public void testUpdateConfigurationRejectsWhenEthNotStarted() {
+        mEthernetServiceImpl.mStarted.set(false);
+        assertThrows(IllegalStateException.class, () -> {
+            mEthernetServiceImpl.updateConfiguration(
+                    "" /* iface */, UPDATE_REQUEST, null /* listener */);
+        });
+    }
+
+    @Test
+    public void testConnectNetworkRejectsWhenEthNotStarted() {
+        mEthernetServiceImpl.mStarted.set(false);
+        assertThrows(IllegalStateException.class, () -> {
+            mEthernetServiceImpl.connectNetwork("" /* iface */, null /* listener */);
+        });
+    }
+
+    @Test
+    public void testDisconnectNetworkRejectsWhenEthNotStarted() {
+        mEthernetServiceImpl.mStarted.set(false);
+        assertThrows(IllegalStateException.class, () -> {
+            mEthernetServiceImpl.disconnectNetwork("" /* iface */, null /* listener */);
+        });
+    }
+
+    @Test
+    public void testUpdateConfigurationRejectsNullIface() {
+        assertThrows(NullPointerException.class, () -> {
+            mEthernetServiceImpl.updateConfiguration(null, UPDATE_REQUEST, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testConnectNetworkRejectsNullIface() {
+        assertThrows(NullPointerException.class, () -> {
+            mEthernetServiceImpl.connectNetwork(null /* iface */, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testDisconnectNetworkRejectsNullIface() {
+        assertThrows(NullPointerException.class, () -> {
+            mEthernetServiceImpl.disconnectNetwork(null /* iface */, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testUpdateConfigurationWithCapabilitiesRejectsWithoutAutomotiveFeature() {
+        toggleAutomotiveFeature(false);
+        assertThrows(UnsupportedOperationException.class, () -> {
+            mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testUpdateConfigurationWithCapabilitiesWithAutomotiveFeature() {
+        toggleAutomotiveFeature(false);
+        mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST_WITHOUT_CAPABILITIES,
+                NULL_LISTENER);
+        verify(mEthernetTracker).updateConfiguration(eq(TEST_IFACE),
+                eq(UPDATE_REQUEST_WITHOUT_CAPABILITIES.getIpConfiguration()),
+                eq(UPDATE_REQUEST_WITHOUT_CAPABILITIES.getNetworkCapabilities()), isNull());
+    }
+
+    @Test
+    public void testConnectNetworkRejectsWithoutAutomotiveFeature() {
+        toggleAutomotiveFeature(false);
+        assertThrows(UnsupportedOperationException.class, () -> {
+            mEthernetServiceImpl.connectNetwork("" /* iface */, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testDisconnectNetworkRejectsWithoutAutomotiveFeature() {
+        toggleAutomotiveFeature(false);
+        assertThrows(UnsupportedOperationException.class, () -> {
+            mEthernetServiceImpl.disconnectNetwork("" /* iface */, NULL_LISTENER);
+        });
+    }
+
+    private void denyManageEthPermission() {
+        doThrow(new SecurityException("")).when(mContext)
+                .enforceCallingOrSelfPermission(
+                        eq(Manifest.permission.MANAGE_ETHERNET_NETWORKS), anyString());
+    }
+
+    private void denyManageTestNetworksPermission() {
+        doThrow(new SecurityException("")).when(mContext)
+                .enforceCallingOrSelfPermission(
+                        eq(Manifest.permission.MANAGE_TEST_NETWORKS), anyString());
+    }
+
+    @Test
+    public void testUpdateConfigurationRejectsWithoutManageEthPermission() {
+        denyManageEthPermission();
+        assertThrows(SecurityException.class, () -> {
+            mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testConnectNetworkRejectsWithoutManageEthPermission() {
+        denyManageEthPermission();
+        assertThrows(SecurityException.class, () -> {
+            mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testDisconnectNetworkRejectsWithoutManageEthPermission() {
+        denyManageEthPermission();
+        assertThrows(SecurityException.class, () -> {
+            mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
+        });
+    }
+
+    private void enableTestInterface() {
+        when(mEthernetTracker.isValidTestInterface(eq(TEST_IFACE))).thenReturn(true);
+    }
+
+    @Test
+    public void testUpdateConfigurationRejectsTestRequestWithoutTestPermission() {
+        enableTestInterface();
+        denyManageTestNetworksPermission();
+        assertThrows(SecurityException.class, () -> {
+            mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testConnectNetworkRejectsTestRequestWithoutTestPermission() {
+        enableTestInterface();
+        denyManageTestNetworksPermission();
+        assertThrows(SecurityException.class, () -> {
+            mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testDisconnectNetworkRejectsTestRequestWithoutTestPermission() {
+        enableTestInterface();
+        denyManageTestNetworksPermission();
+        assertThrows(SecurityException.class, () -> {
+            mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testUpdateConfiguration() {
+        mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST, NULL_LISTENER);
+        verify(mEthernetTracker).updateConfiguration(
+                eq(TEST_IFACE),
+                eq(UPDATE_REQUEST.getIpConfiguration()),
+                eq(UPDATE_REQUEST.getNetworkCapabilities()), eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testConnectNetwork() {
+        mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER);
+        verify(mEthernetTracker).connectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testDisconnectNetwork() {
+        mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
+        verify(mEthernetTracker).disconnectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testUpdateConfigurationAcceptsTestRequestWithNullCapabilities() {
+        enableTestInterface();
+        final EthernetNetworkUpdateRequest request =
+                new EthernetNetworkUpdateRequest
+                        .Builder()
+                        .setIpConfiguration(new IpConfiguration()).build();
+        mEthernetServiceImpl.updateConfiguration(TEST_IFACE, request, NULL_LISTENER);
+        verify(mEthernetTracker).updateConfiguration(eq(TEST_IFACE),
+                eq(request.getIpConfiguration()),
+                eq(request.getNetworkCapabilities()), isNull());
+    }
+
+    @Test
+    public void testUpdateConfigurationAcceptsRequestWithNullIpConfiguration() {
+        mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST_WITHOUT_IP_CONFIG,
+                NULL_LISTENER);
+        verify(mEthernetTracker).updateConfiguration(eq(TEST_IFACE),
+                eq(UPDATE_REQUEST_WITHOUT_IP_CONFIG.getIpConfiguration()),
+                eq(UPDATE_REQUEST_WITHOUT_IP_CONFIG.getNetworkCapabilities()), isNull());
+    }
+
+    @Test
+    public void testUpdateConfigurationRejectsInvalidTestRequest() {
+        enableTestInterface();
+        assertThrows(IllegalArgumentException.class, () -> {
+            mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST, NULL_LISTENER);
+        });
+    }
+
+    private EthernetNetworkUpdateRequest createTestNetworkUpdateRequest() {
+        final NetworkCapabilities nc =  new NetworkCapabilities
+                .Builder(UPDATE_REQUEST.getNetworkCapabilities())
+                .addTransportType(TRANSPORT_TEST).build();
+
+        return new EthernetNetworkUpdateRequest
+                .Builder(UPDATE_REQUEST)
+                .setNetworkCapabilities(nc).build();
+    }
+
+    @Test
+    public void testUpdateConfigurationForTestRequestDoesNotRequireAutoOrEthernetPermission() {
+        enableTestInterface();
+        toggleAutomotiveFeature(false);
+        denyManageEthPermission();
+        final EthernetNetworkUpdateRequest request = createTestNetworkUpdateRequest();
+
+        mEthernetServiceImpl.updateConfiguration(TEST_IFACE, request, NULL_LISTENER);
+        verify(mEthernetTracker).updateConfiguration(
+                eq(TEST_IFACE),
+                eq(request.getIpConfiguration()),
+                eq(request.getNetworkCapabilities()), eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testConnectNetworkForTestRequestDoesNotRequireAutoOrNetPermission() {
+        enableTestInterface();
+        toggleAutomotiveFeature(false);
+        denyManageEthPermission();
+
+        mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER);
+        verify(mEthernetTracker).connectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testDisconnectNetworkForTestRequestDoesNotRequireAutoOrNetPermission() {
+        enableTestInterface();
+        toggleAutomotiveFeature(false);
+        denyManageEthPermission();
+
+        mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
+        verify(mEthernetTracker).disconnectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER));
+    }
+
+    private void denyPermissions(String... permissions) {
+        for (String permission: permissions) {
+            doReturn(PackageManager.PERMISSION_DENIED).when(mContext)
+                    .checkCallingOrSelfPermission(eq(permission));
+        }
+    }
+
+    @Test
+    public void testSetEthernetEnabled() {
+        denyPermissions(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+        mEthernetServiceImpl.setEthernetEnabled(true);
+        verify(mEthernetTracker).setEthernetEnabled(true);
+        reset(mEthernetTracker);
+
+        denyPermissions(Manifest.permission.NETWORK_STACK);
+        mEthernetServiceImpl.setEthernetEnabled(false);
+        verify(mEthernetTracker).setEthernetEnabled(false);
+        reset(mEthernetTracker);
+
+        denyPermissions(Manifest.permission.NETWORK_SETTINGS);
+        try {
+            mEthernetServiceImpl.setEthernetEnabled(true);
+            fail("Should get SecurityException");
+        } catch (SecurityException e) { }
+        verify(mEthernetTracker, never()).setEthernetEnabled(false);
+    }
+}
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
new file mode 100644
index 0000000..b1831c4
--- /dev/null
+++ b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
@@ -0,0 +1,456 @@
+/*
+ * 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 com.android.server.ethernet;
+
+import static android.net.TestNetworkManager.TEST_TAP_PREFIX;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.EthernetManager;
+import android.net.InetAddresses;
+import android.net.INetworkInterfaceOutcomeReceiver;
+import android.net.IEthernetServiceListener;
+import android.net.INetd;
+import android.net.IpConfiguration;
+import android.net.IpConfiguration.IpAssignment;
+import android.net.IpConfiguration.ProxySettings;
+import android.net.InterfaceConfigurationParcel;
+import android.net.LinkAddress;
+import android.net.NetworkCapabilities;
+import android.net.StaticIpConfiguration;
+import android.os.HandlerThread;
+import android.os.RemoteException;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.connectivity.resources.R;
+import com.android.testutils.HandlerUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.net.InetAddress;
+import java.util.ArrayList;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class EthernetTrackerTest {
+    private static final String TEST_IFACE = "test123";
+    private static final int TIMEOUT_MS = 1_000;
+    private static final String THREAD_NAME = "EthernetServiceThread";
+    private static final INetworkInterfaceOutcomeReceiver NULL_LISTENER = null;
+    private EthernetTracker tracker;
+    private HandlerThread mHandlerThread;
+    @Mock private Context mContext;
+    @Mock private EthernetNetworkFactory mFactory;
+    @Mock private INetd mNetd;
+    @Mock private EthernetTracker.Dependencies mDeps;
+
+    @Before
+    public void setUp() throws RemoteException {
+        MockitoAnnotations.initMocks(this);
+        initMockResources();
+        when(mFactory.updateInterfaceLinkState(anyString(), anyBoolean(), any())).thenReturn(false);
+        when(mNetd.interfaceGetList()).thenReturn(new String[0]);
+        mHandlerThread = new HandlerThread(THREAD_NAME);
+        mHandlerThread.start();
+        tracker = new EthernetTracker(mContext, mHandlerThread.getThreadHandler(), mFactory, mNetd,
+                mDeps);
+    }
+
+    @After
+    public void cleanUp() {
+        mHandlerThread.quitSafely();
+    }
+
+    private void initMockResources() {
+        when(mDeps.getInterfaceRegexFromResource(eq(mContext))).thenReturn("");
+        when(mDeps.getInterfaceConfigFromResource(eq(mContext))).thenReturn(new String[0]);
+    }
+
+    private void waitForIdle() {
+        HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
+    }
+
+    /**
+     * Test: Creation of various valid static IP configurations
+     */
+    @Test
+    public void createStaticIpConfiguration() {
+        // Empty gives default StaticIPConfiguration object
+        assertStaticConfiguration(new StaticIpConfiguration(), "");
+
+        // Setting only the IP address properly cascades and assumes defaults
+        assertStaticConfiguration(new StaticIpConfiguration.Builder()
+                .setIpAddress(new LinkAddress("192.0.2.10/24")).build(), "ip=192.0.2.10/24");
+
+        final ArrayList<InetAddress> dnsAddresses = new ArrayList<>();
+        dnsAddresses.add(InetAddresses.parseNumericAddress("4.4.4.4"));
+        dnsAddresses.add(InetAddresses.parseNumericAddress("8.8.8.8"));
+        // Setting other fields properly cascades them
+        assertStaticConfiguration(new StaticIpConfiguration.Builder()
+                .setIpAddress(new LinkAddress("192.0.2.10/24"))
+                .setDnsServers(dnsAddresses)
+                .setGateway(InetAddresses.parseNumericAddress("192.0.2.1"))
+                .setDomains("android").build(),
+                "ip=192.0.2.10/24 dns=4.4.4.4,8.8.8.8 gateway=192.0.2.1 domains=android");
+
+        // Verify order doesn't matter
+        assertStaticConfiguration(new StaticIpConfiguration.Builder()
+                .setIpAddress(new LinkAddress("192.0.2.10/24"))
+                .setDnsServers(dnsAddresses)
+                .setGateway(InetAddresses.parseNumericAddress("192.0.2.1"))
+                .setDomains("android").build(),
+                "domains=android ip=192.0.2.10/24 gateway=192.0.2.1 dns=4.4.4.4,8.8.8.8 ");
+    }
+
+    /**
+     * Test: Attempt creation of various bad static IP configurations
+     */
+    @Test
+    public void createStaticIpConfiguration_Bad() {
+        assertStaticConfigurationFails("ip=192.0.2.1/24 gateway= blah=20.20.20.20");  // Unknown key
+        assertStaticConfigurationFails("ip=192.0.2.1");  // mask is missing
+        assertStaticConfigurationFails("ip=a.b.c");  // not a valid ip address
+        assertStaticConfigurationFails("dns=4.4.4.4,1.2.3.A");  // not valid ip address in dns
+        assertStaticConfigurationFails("=");  // Key and value is empty
+        assertStaticConfigurationFails("ip=");  // Value is empty
+        assertStaticConfigurationFails("ip=192.0.2.1/24 gateway=");  // Gateway is empty
+    }
+
+    private void assertStaticConfigurationFails(String config) {
+        try {
+            EthernetTracker.parseStaticIpConfiguration(config);
+            fail("Expected to fail: " + config);
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
+    private void assertStaticConfiguration(StaticIpConfiguration expectedStaticIpConfig,
+                String configAsString) {
+        final IpConfiguration expectedIpConfiguration = new IpConfiguration();
+        expectedIpConfiguration.setIpAssignment(IpAssignment.STATIC);
+        expectedIpConfiguration.setProxySettings(ProxySettings.NONE);
+        expectedIpConfiguration.setStaticIpConfiguration(expectedStaticIpConfig);
+
+        assertEquals(expectedIpConfiguration,
+                EthernetTracker.parseStaticIpConfiguration(configAsString));
+    }
+
+    private NetworkCapabilities.Builder makeEthernetCapabilitiesBuilder(boolean clearAll) {
+        final NetworkCapabilities.Builder builder =
+                clearAll ? NetworkCapabilities.Builder.withoutDefaultCapabilities()
+                        : new NetworkCapabilities.Builder();
+        return builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED);
+    }
+
+    /**
+     * Test: Attempt to create a capabilties with various valid sets of capabilities/transports
+     */
+    @Test
+    public void createNetworkCapabilities() {
+
+        // Particularly common expected results
+        NetworkCapabilities defaultEthernetCleared =
+                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                        .build();
+
+        NetworkCapabilities ethernetClearedWithCommonCaps =
+                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                        .addCapability(12)
+                        .addCapability(13)
+                        .addCapability(14)
+                        .addCapability(15)
+                        .build();
+
+        // Empty capabilities and transports lists with a "please clear defaults" should
+        // yield an empty capabilities set with TRANPORT_ETHERNET
+        assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "");
+
+        // Empty capabilities and transports without the clear defaults flag should return the
+        // default capabilities set with TRANSPORT_ETHERNET
+        assertParsedNetworkCapabilities(
+                makeEthernetCapabilitiesBuilder(false /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                        .build(),
+                false, "", "");
+
+        // A list of capabilities without the clear defaults flag should return the default
+        // capabilities, mixed with the desired capabilities, and TRANSPORT_ETHERNET
+        assertParsedNetworkCapabilities(
+                makeEthernetCapabilitiesBuilder(false /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                        .addCapability(11)
+                        .addCapability(12)
+                        .build(),
+                false, "11,12", "");
+
+        // Adding a list of capabilities with a clear defaults will leave exactly those capabilities
+        // with a default TRANSPORT_ETHERNET since no overrides are specified
+        assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15", "");
+
+        // Adding any invalid capabilities to the list will cause them to be ignored
+        assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15,65,73", "");
+        assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15,abcdefg", "");
+
+        // Adding a valid override transport will remove the default TRANSPORT_ETHERNET transport
+        // and apply only the override to the capabiltities object
+        assertParsedNetworkCapabilities(
+                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(0)
+                        .build(),
+                true, "", "0");
+        assertParsedNetworkCapabilities(
+                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(1)
+                        .build(),
+                true, "", "1");
+        assertParsedNetworkCapabilities(
+                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(2)
+                        .build(),
+                true, "", "2");
+        assertParsedNetworkCapabilities(
+                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(3)
+                        .build(),
+                true, "", "3");
+
+        // "4" is TRANSPORT_VPN, which is unsupported. Should default back to TRANPORT_ETHERNET
+        assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "4");
+
+        // "5" is TRANSPORT_WIFI_AWARE, which is currently supported due to no legacy TYPE_NONE
+        // conversion. When that becomes available, this test must be updated
+        assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "5");
+
+        // "6" is TRANSPORT_LOWPAN, which is currently supported due to no legacy TYPE_NONE
+        // conversion. When that becomes available, this test must be updated
+        assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "6");
+
+        // Adding an invalid override transport will leave the transport as TRANSPORT_ETHERNET
+        assertParsedNetworkCapabilities(defaultEthernetCleared,true, "", "100");
+        assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "abcdefg");
+
+        // Ensure the adding of both capabilities and transports work
+        assertParsedNetworkCapabilities(
+                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addCapability(12)
+                        .addCapability(13)
+                        .addCapability(14)
+                        .addCapability(15)
+                        .addTransportType(3)
+                        .build(),
+                true, "12,13,14,15", "3");
+
+        // Ensure order does not matter for capability list
+        assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "13,12,15,14", "");
+    }
+
+    private void assertParsedNetworkCapabilities(NetworkCapabilities expectedNetworkCapabilities,
+            boolean clearCapabilties, String configCapabiltiies,String configTransports) {
+        assertEquals(expectedNetworkCapabilities,
+                EthernetTracker.createNetworkCapabilities(clearCapabilties, configCapabiltiies,
+                        configTransports).build());
+    }
+
+    @Test
+    public void testCreateEthernetTrackerConfigReturnsCorrectValue() {
+        final String capabilities = "2";
+        final String ipConfig = "3";
+        final String transport = "4";
+        final String configString = String.join(";", TEST_IFACE, capabilities, ipConfig, transport);
+
+        final EthernetTracker.EthernetTrackerConfig config =
+                EthernetTracker.createEthernetTrackerConfig(configString);
+
+        assertEquals(TEST_IFACE, config.mIface);
+        assertEquals(capabilities, config.mCapabilities);
+        assertEquals(ipConfig, config.mIpConfig);
+        assertEquals(transport, config.mTransport);
+    }
+
+    @Test
+    public void testCreateEthernetTrackerConfigThrowsNpeWithNullInput() {
+        assertThrows(NullPointerException.class,
+                () -> EthernetTracker.createEthernetTrackerConfig(null));
+    }
+
+    @Test
+    public void testUpdateConfiguration() {
+        final NetworkCapabilities capabilities = new NetworkCapabilities.Builder().build();
+        final LinkAddress linkAddr = new LinkAddress("192.0.2.2/25");
+        final StaticIpConfiguration staticIpConfig =
+                new StaticIpConfiguration.Builder().setIpAddress(linkAddr).build();
+        final IpConfiguration ipConfig =
+                new IpConfiguration.Builder().setStaticIpConfiguration(staticIpConfig).build();
+        final INetworkInterfaceOutcomeReceiver listener = null;
+
+        tracker.updateConfiguration(TEST_IFACE, ipConfig, capabilities, listener);
+        waitForIdle();
+
+        verify(mFactory).updateInterface(
+                eq(TEST_IFACE), eq(ipConfig), eq(capabilities), eq(listener));
+    }
+
+    @Test
+    public void testConnectNetworkCorrectlyCallsFactory() {
+        tracker.connectNetwork(TEST_IFACE, NULL_LISTENER);
+        waitForIdle();
+
+        verify(mFactory).updateInterfaceLinkState(eq(TEST_IFACE), eq(true /* up */),
+                eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testDisconnectNetworkCorrectlyCallsFactory() {
+        tracker.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
+        waitForIdle();
+
+        verify(mFactory).updateInterfaceLinkState(eq(TEST_IFACE), eq(false /* up */),
+                eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testIsValidTestInterfaceIsFalseWhenTestInterfacesAreNotIncluded() {
+        final String validIfaceName = TEST_TAP_PREFIX + "123";
+        tracker.setIncludeTestInterfaces(false);
+        waitForIdle();
+
+        final boolean isValidTestInterface = tracker.isValidTestInterface(validIfaceName);
+
+        assertFalse(isValidTestInterface);
+    }
+
+    @Test
+    public void testIsValidTestInterfaceIsFalseWhenTestInterfaceNameIsInvalid() {
+        final String invalidIfaceName = "123" + TEST_TAP_PREFIX;
+        tracker.setIncludeTestInterfaces(true);
+        waitForIdle();
+
+        final boolean isValidTestInterface = tracker.isValidTestInterface(invalidIfaceName);
+
+        assertFalse(isValidTestInterface);
+    }
+
+    @Test
+    public void testIsValidTestInterfaceIsTrueWhenTestInterfacesIncludedAndValidName() {
+        final String validIfaceName = TEST_TAP_PREFIX + "123";
+        tracker.setIncludeTestInterfaces(true);
+        waitForIdle();
+
+        final boolean isValidTestInterface = tracker.isValidTestInterface(validIfaceName);
+
+        assertTrue(isValidTestInterface);
+    }
+
+    public static class EthernetStateListener extends IEthernetServiceListener.Stub {
+        @Override
+        public void onEthernetStateChanged(int state) { }
+
+        @Override
+        public void onInterfaceStateChanged(String iface, int state, int role,
+                IpConfiguration configuration) { }
+    }
+
+    @Test
+    public void testListenEthernetStateChange() throws Exception {
+        final String testIface = "testtap123";
+        final String testHwAddr = "11:22:33:44:55:66";
+        final InterfaceConfigurationParcel ifaceParcel = new InterfaceConfigurationParcel();
+        ifaceParcel.ifName = testIface;
+        ifaceParcel.hwAddr = testHwAddr;
+        ifaceParcel.flags = new String[] {INetd.IF_STATE_UP};
+
+        tracker.setIncludeTestInterfaces(true);
+        waitForIdle();
+
+        when(mNetd.interfaceGetList()).thenReturn(new String[] {testIface});
+        when(mNetd.interfaceGetCfg(eq(testIface))).thenReturn(ifaceParcel);
+        doReturn(new String[] {testIface}).when(mFactory).getAvailableInterfaces(anyBoolean());
+        doReturn(EthernetManager.STATE_LINK_UP).when(mFactory).getInterfaceState(eq(testIface));
+
+        final EthernetStateListener listener = spy(new EthernetStateListener());
+        tracker.addListener(listener, true /* canUseRestrictedNetworks */);
+        // Check default state.
+        waitForIdle();
+        verify(listener).onInterfaceStateChanged(eq(testIface), eq(EthernetManager.STATE_LINK_UP),
+                anyInt(), any());
+        verify(listener).onEthernetStateChanged(eq(EthernetManager.ETHERNET_STATE_ENABLED));
+        reset(listener);
+
+        doReturn(EthernetManager.STATE_ABSENT).when(mFactory).getInterfaceState(eq(testIface));
+        tracker.setEthernetEnabled(false);
+        waitForIdle();
+        verify(mFactory).removeInterface(eq(testIface));
+        verify(listener).onEthernetStateChanged(eq(EthernetManager.ETHERNET_STATE_DISABLED));
+        verify(listener).onInterfaceStateChanged(eq(testIface), eq(EthernetManager.STATE_ABSENT),
+                anyInt(), any());
+        reset(listener);
+
+        doReturn(EthernetManager.STATE_LINK_UP).when(mFactory).getInterfaceState(eq(testIface));
+        tracker.setEthernetEnabled(true);
+        waitForIdle();
+        verify(mFactory).addInterface(eq(testIface), eq(testHwAddr), any(), any());
+        verify(listener).onEthernetStateChanged(eq(EthernetManager.ETHERNET_STATE_ENABLED));
+        verify(listener).onInterfaceStateChanged(eq(testIface), eq(EthernetManager.STATE_LINK_UP),
+                anyInt(), any());
+    }
+}