Merge "Add a method to apply a set of network preferences to a user profile"
diff --git a/OWNERS_core_networking b/OWNERS_core_networking
index 6847c74..bc1d002 100644
--- a/OWNERS_core_networking
+++ b/OWNERS_core_networking
@@ -12,6 +12,7 @@
 maze@google.com
 nuccachen@google.com
 paulhu@google.com
+prohr@google.com
 reminv@google.com
 satk@google.com
 waynema@google.com
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 90312a4..06d4416 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -61,6 +61,9 @@
       "path": "frameworks/base/core/java/android/net"
     },
     {
+      "path": "frameworks/opt/net/ethernet"
+    },
+    {
       "path": "packages/modules/NetworkStack"
     },
     {
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 1747450..e4faf85 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -4042,11 +4042,11 @@
                 config = new NativeNetworkConfig(nai.network.getNetId(), NativeNetworkType.VIRTUAL,
                         INetd.PERMISSION_NONE,
                         (nai.networkAgentConfig == null || !nai.networkAgentConfig.allowBypass),
-                        getVpnType(nai));
+                        getVpnType(nai), /*excludeLocalRoutes=*/ false);
             } else {
                 config = new NativeNetworkConfig(nai.network.getNetId(), NativeNetworkType.PHYSICAL,
                         getNetworkPermission(nai.networkCapabilities), /*secure=*/ false,
-                        VpnManager.TYPE_VPN_NONE);
+                        VpnManager.TYPE_VPN_NONE, /*excludeLocalRoutes=*/ false);
             }
             mNetd.networkCreate(config);
             mDnsResolver.createNetworkCache(nai.network.getNetId());
diff --git a/tests/cts/hostside/Android.bp b/tests/cts/hostside/Android.bp
index f72a458..b684068 100644
--- a/tests/cts/hostside/Android.bp
+++ b/tests/cts/hostside/Android.bp
@@ -25,6 +25,9 @@
         "cts-tradefed",
         "tradefed",
     ],
+    static_libs: [
+        "modules-utils-build-testing",
+    ],
     // Tag this module as a cts test artifact
     test_suites: [
         "cts",
diff --git a/tests/cts/hostside/app/Android.bp b/tests/cts/hostside/app/Android.bp
index 63572c3..12e7d33 100644
--- a/tests/cts/hostside/app/Android.bp
+++ b/tests/cts/hostside/app/Android.bp
@@ -18,12 +18,8 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-android_test_helper_app {
-    name: "CtsHostsideNetworkTestsApp",
-    defaults: [
-        "cts_support_defaults",
-        "framework-connectivity-test-defaults",
-    ],
+java_defaults {
+    name: "CtsHostsideNetworkTestsAppDefaults",
     platform_apis: true,
     static_libs: [
         "CtsHostsideNetworkTestsAidl",
@@ -48,3 +44,28 @@
         "sts",
     ],
 }
+
+android_test_helper_app {
+    name: "CtsHostsideNetworkTestsApp",
+    defaults: [
+        "cts_support_defaults",
+        "framework-connectivity-test-defaults",
+        "CtsHostsideNetworkTestsAppDefaults",
+    ],
+    static_libs: [
+        "NetworkStackApiStableShims",
+    ],
+}
+
+android_test_helper_app {
+    name: "CtsHostsideNetworkTestsAppNext",
+    defaults: [
+        "cts_support_defaults",
+        "framework-connectivity-test-defaults",
+        "CtsHostsideNetworkTestsAppDefaults",
+        "ConnectivityNextEnableDefaults",
+    ],
+    static_libs: [
+        "NetworkStackApiCurrentShims",
+    ],
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java
index 855abf7..449454e 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java
@@ -18,17 +18,26 @@
 
 import android.content.Intent;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.IpPrefix;
 import android.net.Network;
+import android.net.NetworkUtils;
 import android.net.ProxyInfo;
 import android.net.VpnService;
 import android.os.ParcelFileDescriptor;
 import android.text.TextUtils;
 import android.util.Log;
+import android.util.Pair;
+
+import com.android.modules.utils.build.SdkLevel;
+import com.android.networkstack.apishim.VpnServiceBuilderShimImpl;
+import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
+import com.android.networkstack.apishim.common.VpnServiceBuilderShim;
 
 import java.io.IOException;
 import java.net.InetAddress;
-import java.net.UnknownHostException;
 import java.util.ArrayList;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
 
 public class MyVpnService extends VpnService {
 
@@ -67,39 +76,62 @@
                 (underlyingNetworks != null) ? underlyingNetworks.toArray(new Network[0]) : null);
     }
 
-    private void start(String packageName, Intent intent) {
-        Builder builder = new Builder();
+    private String parseIpAndMaskListArgument(String packageName, Intent intent, String argName,
+            BiConsumer<InetAddress, Integer> consumer) {
+        final String addresses = intent.getStringExtra(packageName + "." + argName);
 
-        String addresses = intent.getStringExtra(packageName + ".addresses");
-        if (addresses != null) {
-            String[] addressArray = addresses.split(",");
-            for (int i = 0; i < addressArray.length; i++) {
-                String[] prefixAndMask = addressArray[i].split("/");
-                try {
-                    InetAddress address = InetAddress.getByName(prefixAndMask[0]);
-                    int prefixLength = Integer.parseInt(prefixAndMask[1]);
-                    builder.addAddress(address, prefixLength);
-                } catch (UnknownHostException|NumberFormatException|
-                         ArrayIndexOutOfBoundsException e) {
-                    continue;
-                }
-            }
+        if (TextUtils.isEmpty(addresses)) {
+            return null;
         }
 
-        String routes = intent.getStringExtra(packageName + ".routes");
-        if (routes != null) {
-            String[] routeArray = routes.split(",");
-            for (int i = 0; i < routeArray.length; i++) {
-                String[] prefixAndMask = routeArray[i].split("/");
+        final String[] addressesArray = addresses.split(",");
+        for (String address : addressesArray) {
+            final Pair<InetAddress, Integer> ipAndMask = NetworkUtils.parseIpAndMask(address);
+            consumer.accept(ipAndMask.first, ipAndMask.second);
+        }
+
+        return addresses;
+    }
+
+    private String parseIpPrefixListArgument(String packageName, Intent intent, String argName,
+            Consumer<IpPrefix> consumer) {
+        return parseIpAndMaskListArgument(packageName, intent, argName,
+                (inetAddress, prefixLength) -> consumer.accept(
+                        new IpPrefix(inetAddress, prefixLength)));
+    }
+
+    private void start(String packageName, Intent intent) {
+        Builder builder = new Builder();
+        VpnServiceBuilderShim vpnServiceBuilderShim = VpnServiceBuilderShimImpl.newInstance();
+
+        final String addresses = parseIpAndMaskListArgument(packageName, intent, "addresses",
+                builder::addAddress);
+
+        String addedRoutes;
+        if (SdkLevel.isAtLeastT() && intent.getBooleanExtra(packageName + ".addRoutesByIpPrefix",
+                false)) {
+            addedRoutes = parseIpPrefixListArgument(packageName, intent, "routes", (prefix) -> {
                 try {
-                    InetAddress address = InetAddress.getByName(prefixAndMask[0]);
-                    int prefixLength = Integer.parseInt(prefixAndMask[1]);
-                    builder.addRoute(address, prefixLength);
-                } catch (UnknownHostException|NumberFormatException|
-                         ArrayIndexOutOfBoundsException e) {
-                    continue;
+                    vpnServiceBuilderShim.addRoute(builder, prefix);
+                } catch (UnsupportedApiLevelException e) {
+                    throw new RuntimeException(e);
                 }
-            }
+            });
+        } else {
+            addedRoutes = parseIpAndMaskListArgument(packageName, intent, "routes",
+                    builder::addRoute);
+        }
+
+        String excludedRoutes = null;
+        if (SdkLevel.isAtLeastT()) {
+            excludedRoutes = parseIpPrefixListArgument(packageName, intent, "excludedRoutes",
+                    (prefix) -> {
+                        try {
+                            vpnServiceBuilderShim.excludeRoute(builder, prefix);
+                        } catch (UnsupportedApiLevelException e) {
+                            throw new RuntimeException(e);
+                        }
+                    });
         }
 
         String allowed = intent.getStringExtra(packageName + ".allowedapplications");
@@ -152,7 +184,8 @@
 
         Log.i(TAG, "Establishing VPN,"
                 + " addresses=" + addresses
-                + " routes=" + routes
+                + " addedRoutes=" + addedRoutes
+                + " excludedRoutes=" + excludedRoutes
                 + " allowedApplications=" + allowed
                 + " disallowedApplications=" + disallowed);
 
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
index 65ea4b1..708d600 100755
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
@@ -298,28 +298,54 @@
         mActivity.startService(intent);
     }
 
-    private void establishVpn(String[] addresses, String[] routes, String allowedApplications,
-            String disallowedApplications, @Nullable ProxyInfo proxyInfo,
-            @Nullable ArrayList<Network> underlyingNetworks, boolean isAlwaysMetered)
+    private void establishVpn(String[] addresses, String[] routes, String[] excludedRoutes,
+            String allowedApplications, String disallowedApplications,
+            @Nullable ProxyInfo proxyInfo, @Nullable ArrayList<Network> underlyingNetworks,
+            boolean isAlwaysMetered, boolean addRoutesByIpPrefix)
             throws Exception {
         final Intent intent = new Intent(mActivity, MyVpnService.class)
                 .putExtra(mPackageName + ".cmd", MyVpnService.CMD_CONNECT)
                 .putExtra(mPackageName + ".addresses", TextUtils.join(",", addresses))
                 .putExtra(mPackageName + ".routes", TextUtils.join(",", routes))
+                .putExtra(mPackageName + ".excludedRoutes", TextUtils.join(",", excludedRoutes))
                 .putExtra(mPackageName + ".allowedapplications", allowedApplications)
                 .putExtra(mPackageName + ".disallowedapplications", disallowedApplications)
                 .putExtra(mPackageName + ".httpProxy", proxyInfo)
                 .putParcelableArrayListExtra(
                         mPackageName + ".underlyingNetworks", underlyingNetworks)
-                .putExtra(mPackageName + ".isAlwaysMetered", isAlwaysMetered);
+                .putExtra(mPackageName + ".isAlwaysMetered", isAlwaysMetered)
+                .putExtra(mPackageName + ".addRoutesByIpPrefix", addRoutesByIpPrefix);
         mActivity.startService(intent);
     }
 
     // TODO: Consider replacing arguments with a Builder.
     private void startVpn(
-        String[] addresses, String[] routes, String allowedApplications,
-        String disallowedApplications, @Nullable ProxyInfo proxyInfo,
-        @Nullable ArrayList<Network> underlyingNetworks, boolean isAlwaysMetered) throws Exception {
+            String[] addresses, String[] routes, String allowedApplications,
+            String disallowedApplications, @Nullable ProxyInfo proxyInfo,
+            @Nullable ArrayList<Network> underlyingNetworks, boolean isAlwaysMetered)
+            throws Exception {
+        startVpn(addresses, routes, new String[0] /* excludedRoutes */, allowedApplications,
+                disallowedApplications, proxyInfo, underlyingNetworks, isAlwaysMetered);
+    }
+
+    private void startVpn(
+            String[] addresses, String[] routes, String[] excludedRoutes,
+            String allowedApplications, String disallowedApplications,
+            @Nullable ProxyInfo proxyInfo,
+            @Nullable ArrayList<Network> underlyingNetworks, boolean isAlwaysMetered)
+            throws Exception {
+        startVpn(addresses, routes, new String[0] /* excludedRoutes */, allowedApplications,
+                disallowedApplications, proxyInfo, underlyingNetworks, isAlwaysMetered,
+                false /* addRoutesByIpPrefix */);
+    }
+
+    private void startVpn(
+            String[] addresses, String[] routes, String[] excludedRoutes,
+            String allowedApplications, String disallowedApplications,
+            @Nullable ProxyInfo proxyInfo,
+            @Nullable ArrayList<Network> underlyingNetworks, boolean isAlwaysMetered,
+            boolean addRoutesByIpPrefix)
+            throws Exception {
         prepareVpn();
 
         // Register a callback so we will be notified when our VPN comes up.
@@ -340,8 +366,8 @@
         mCM.registerNetworkCallback(request, mCallback);  // Unregistered in tearDown.
 
         // Start the service and wait up for TIMEOUT_MS ms for the VPN to come up.
-        establishVpn(addresses, routes, allowedApplications, disallowedApplications, proxyInfo,
-                underlyingNetworks, isAlwaysMetered);
+        establishVpn(addresses, routes, excludedRoutes, allowedApplications, disallowedApplications,
+                proxyInfo, underlyingNetworks, isAlwaysMetered, addRoutesByIpPrefix);
         synchronized (mLock) {
             if (mNetwork == null) {
                  Log.i(TAG, "bf mLock");
@@ -561,6 +587,12 @@
     }
 
     private void checkUdpEcho(String to, String expectedFrom) throws IOException {
+        checkUdpEcho(to, expectedFrom, expectedFrom != null);
+    }
+
+    private void checkUdpEcho(String to, String expectedFrom,
+            boolean expectConnectionOwnerIsVisible)
+            throws IOException {
         DatagramSocket s;
         InetAddress address = InetAddress.getByName(to);
         if (address instanceof Inet6Address) {  // http://b/18094870
@@ -584,7 +616,7 @@
         try {
             if (expectedFrom != null) {
                 s.send(p);
-                checkConnectionOwnerUidUdp(s, true);
+                checkConnectionOwnerUidUdp(s, expectConnectionOwnerIsVisible);
                 s.receive(p);
                 MoreAsserts.assertEquals(data, p.getData());
             } else {
@@ -593,7 +625,7 @@
                     s.receive(p);
                     fail("Received unexpected reply");
                 } catch (IOException expected) {
-                    checkConnectionOwnerUidUdp(s, false);
+                    checkConnectionOwnerUidUdp(s, expectConnectionOwnerIsVisible);
                 }
             }
         } finally {
@@ -601,19 +633,38 @@
         }
     }
 
+    private void checkTrafficOnVpn(String destination) throws Exception {
+        final InetAddress address = InetAddress.getByName(destination);
+
+        if (address instanceof Inet6Address) {
+            checkUdpEcho(destination, "2001:db8:1:2::ffe");
+            checkTcpReflection(destination, "2001:db8:1:2::ffe");
+            checkPing(destination);
+        } else {
+            checkUdpEcho(destination, "192.0.2.2");
+            checkTcpReflection(destination, "192.0.2.2");
+        }
+
+    }
+
+    private void checkNoTrafficOnVpn(String destination) throws IOException {
+        checkUdpEcho(destination, null);
+        checkTcpReflection(destination, null);
+    }
+
     private void checkTrafficOnVpn() throws Exception {
-        checkUdpEcho("192.0.2.251", "192.0.2.2");
-        checkUdpEcho("2001:db8:dead:beef::f00", "2001:db8:1:2::ffe");
-        checkPing("2001:db8:dead:beef::f00");
-        checkTcpReflection("192.0.2.252", "192.0.2.2");
-        checkTcpReflection("2001:db8:dead:beef::f00", "2001:db8:1:2::ffe");
+        checkTrafficOnVpn("192.0.2.251");
+        checkTrafficOnVpn("2001:db8:dead:beef::f00");
     }
 
     private void checkNoTrafficOnVpn() throws Exception {
-        checkUdpEcho("192.0.2.251", null);
-        checkUdpEcho("2001:db8:dead:beef::f00", null);
-        checkTcpReflection("192.0.2.252", null);
-        checkTcpReflection("2001:db8:dead:beef::f00", null);
+        checkNoTrafficOnVpn("192.0.2.251");
+        checkNoTrafficOnVpn("2001:db8:dead:beef::f00");
+    }
+
+    private void checkTrafficBypassesVpn(String destination) throws Exception {
+        checkUdpEcho(destination, null, true /* expectVpnOwnedConnection */);
+        checkTcpReflection(destination, null);
     }
 
     private FileDescriptor openSocketFd(String host, int port, int timeoutMs) throws Exception {
@@ -974,9 +1025,9 @@
         }
         Log.i(TAG, "Append shell app to disallowedApps: " + disallowedApps);
         startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
-                 new String[] {"192.0.2.0/24", "2001:db8::/32"},
-                 "", disallowedApps, null, null /* underlyingNetworks */,
-                 false /* isAlwaysMetered */);
+                new String[] {"192.0.2.0/24", "2001:db8::/32"},
+                "", disallowedApps, null, null /* underlyingNetworks */,
+                false /* isAlwaysMetered */);
 
         assertSocketStillOpen(localFd, TEST_HOST);
         assertSocketStillOpen(remoteFd, TEST_HOST);
@@ -989,6 +1040,74 @@
     }
 
     @Test
+    public void testExcludedRoutes() throws Exception {
+        if (!supportedHardware()) return;
+        if (!SdkLevel.isAtLeastT()) return;
+
+        // Shell app must not be put in here or it would kill the ADB-over-network use case
+        String allowedApps = mRemoteSocketFactoryClient.getPackageName() + "," + mPackageName;
+        startVpn(new String[]{"192.0.2.2/32", "2001:db8:1:2::ffe/128"} /* addresses */,
+                new String[]{"0.0.0.0/0", "::/0"} /* routes */,
+                new String[]{"192.0.2.0/24", "2001:db8::/32"} /* excludedRoutes */,
+                allowedApps, "" /* disallowedApplications */, null /* proxyInfo */,
+                null /* underlyingNetworks */, false /* isAlwaysMetered */);
+
+        // Excluded routes should bypass VPN.
+        checkTrafficBypassesVpn("192.0.2.1");
+        checkTrafficBypassesVpn("2001:db8:dead:beef::f00");
+        // Other routes should go through VPN, since default routes are included.
+        checkTrafficOnVpn("198.51.100.1");
+        checkTrafficOnVpn("2002:db8::1");
+    }
+
+    @Test
+    public void testIncludedRoutes() throws Exception {
+        if (!supportedHardware()) return;
+
+        // Shell app must not be put in here or it would kill the ADB-over-network use case
+        String allowedApps = mRemoteSocketFactoryClient.getPackageName() + "," + mPackageName;
+        startVpn(new String[]{"192.0.2.2/32", "2001:db8:1:2::ffe/128"} /* addresses */,
+                new String[]{"192.0.2.0/24", "2001:db8::/32"} /* routes */,
+                allowedApps, "" /* disallowedApplications */, null /* proxyInfo */,
+                null /* underlyingNetworks */, false /* isAlwaysMetered */);
+
+        // Included routes should go through VPN.
+        checkTrafficOnVpn("192.0.2.1");
+        checkTrafficOnVpn("2001:db8:dead:beef::f00");
+        // Other routes should bypass VPN, since default routes are not included.
+        checkTrafficBypassesVpn("198.51.100.1");
+        checkTrafficBypassesVpn("2002:db8::1");
+    }
+
+    @Test
+    public void testInterleavedRoutes() throws Exception {
+        if (!supportedHardware()) return;
+        if (!SdkLevel.isAtLeastT()) return;
+
+        // Shell app must not be put in here or it would kill the ADB-over-network use case
+        String allowedApps = mRemoteSocketFactoryClient.getPackageName() + "," + mPackageName;
+        startVpn(new String[]{"192.0.2.2/32", "2001:db8:1:2::ffe/128"} /* addresses */,
+                new String[]{"0.0.0.0/0", "192.0.2.0/32", "::/0", "2001:db8::/128"} /* routes */,
+                new String[]{"192.0.2.0/24", "2001:db8::/32"} /* excludedRoutes */,
+                allowedApps, "" /* disallowedApplications */, null /* proxyInfo */,
+                null /* underlyingNetworks */, false /* isAlwaysMetered */,
+                true /* addRoutesByIpPrefix */);
+
+        // Excluded routes should bypass VPN.
+        checkTrafficBypassesVpn("192.0.2.1");
+        checkTrafficBypassesVpn("2001:db8:dead:beef::f00");
+
+        // Included routes inside excluded routes should go through VPN, since the longest common
+        // prefix precedes.
+        checkTrafficOnVpn("192.0.2.0");
+        checkTrafficOnVpn("2001:db8::");
+
+        // Other routes should go through VPN, since default routes are included.
+        checkTrafficOnVpn("198.51.100.1");
+        checkTrafficOnVpn("2002:db8::1");
+    }
+
+    @Test
     public void testGetConnectionOwnerUidSecurity() throws Exception {
         if (!supportedHardware()) return;
 
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
index 89c79d3..cc07fd1 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
@@ -20,6 +20,7 @@
 import com.android.ddmlib.Log;
 import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
 import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.modules.utils.build.testing.DeviceSdkLevel;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.result.CollectingTestListener;
@@ -42,6 +43,7 @@
     protected static final String TAG = "HostsideNetworkTests";
     protected static final String TEST_PKG = "com.android.cts.net.hostside";
     protected static final String TEST_APK = "CtsHostsideNetworkTestsApp.apk";
+    protected static final String TEST_APK_NEXT = "CtsHostsideNetworkTestsAppNext.apk";
     protected static final String TEST_APP2_PKG = "com.android.cts.net.hostside.app2";
     protected static final String TEST_APP2_APK = "CtsHostsideNetworkTestsApp2.apk";
 
@@ -65,8 +67,12 @@
         assertNotNull(mAbi);
         assertNotNull(mCtsBuild);
 
+        DeviceSdkLevel deviceSdkLevel = new DeviceSdkLevel(getDevice());
+        String testApk = deviceSdkLevel.isDeviceAtLeastT() ? TEST_APK_NEXT
+                : TEST_APK;
+
         uninstallPackage(TEST_PKG, false);
-        installPackage(TEST_APK);
+        installPackage(testApk);
     }
 
     @Override
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
index 180015d..3821f87 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
@@ -104,4 +104,16 @@
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest",
                 "testDownloadWithDownloadManagerDisallowed");
     }
+
+    public void testExcludedRoutes() throws Exception {
+        runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testExcludedRoutes");
+    }
+
+    public void testIncludedRoutes() throws Exception {
+        runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testIncludedRoutes");
+    }
+
+    public void testInterleavedRoutes() throws Exception {
+        runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testInterleavedRoutes");
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.java b/tests/cts/net/src/android/net/cts/NsdManagerTest.java
deleted file mode 100644
index 2bcfdc3..0000000
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.java
+++ /dev/null
@@ -1,594 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.net.cts;
-
-import android.content.Context;
-import android.net.nsd.NsdManager;
-import android.net.nsd.NsdServiceInfo;
-import android.platform.test.annotations.AppModeFull;
-import android.test.AndroidTestCase;
-import android.util.Log;
-
-import java.io.IOException;
-import java.net.ServerSocket;
-import java.util.Arrays;
-import java.util.Random;
-import java.util.List;
-import java.util.ArrayList;
-
-@AppModeFull(reason = "Socket cannot bind in instant app mode")
-public class NsdManagerTest extends AndroidTestCase {
-
-    private static final String TAG = "NsdManagerTest";
-    private static final String SERVICE_TYPE = "_nmt._tcp";
-    private static final int TIMEOUT = 2000;
-
-    private static final boolean DBG = false;
-
-    NsdManager mNsdManager;
-
-    NsdManager.RegistrationListener mRegistrationListener;
-    NsdManager.DiscoveryListener mDiscoveryListener;
-    NsdManager.ResolveListener mResolveListener;
-    private NsdServiceInfo mResolvedService;
-
-    public NsdManagerTest() {
-        initRegistrationListener();
-        initDiscoveryListener();
-        initResolveListener();
-    }
-
-    private void initRegistrationListener() {
-        mRegistrationListener = new NsdManager.RegistrationListener() {
-            @Override
-            public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
-                setEvent("onRegistrationFailed", errorCode);
-            }
-
-            @Override
-            public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
-                setEvent("onUnregistrationFailed", errorCode);
-            }
-
-            @Override
-            public void onServiceRegistered(NsdServiceInfo serviceInfo) {
-                setEvent("onServiceRegistered", serviceInfo);
-            }
-
-            @Override
-            public void onServiceUnregistered(NsdServiceInfo serviceInfo) {
-                setEvent("onServiceUnregistered", serviceInfo);
-            }
-        };
-    }
-
-    private void initDiscoveryListener() {
-        mDiscoveryListener = new NsdManager.DiscoveryListener() {
-            @Override
-            public void onStartDiscoveryFailed(String serviceType, int errorCode) {
-                setEvent("onStartDiscoveryFailed", errorCode);
-            }
-
-            @Override
-            public void onStopDiscoveryFailed(String serviceType, int errorCode) {
-                setEvent("onStopDiscoveryFailed", errorCode);
-            }
-
-            @Override
-            public void onDiscoveryStarted(String serviceType) {
-                NsdServiceInfo info = new NsdServiceInfo();
-                info.setServiceType(serviceType);
-                setEvent("onDiscoveryStarted", info);
-            }
-
-            @Override
-            public void onDiscoveryStopped(String serviceType) {
-                NsdServiceInfo info = new NsdServiceInfo();
-                info.setServiceType(serviceType);
-                setEvent("onDiscoveryStopped", info);
-            }
-
-            @Override
-            public void onServiceFound(NsdServiceInfo serviceInfo) {
-                setEvent("onServiceFound", serviceInfo);
-            }
-
-            @Override
-            public void onServiceLost(NsdServiceInfo serviceInfo) {
-                setEvent("onServiceLost", serviceInfo);
-            }
-        };
-    }
-
-    private void initResolveListener() {
-        mResolveListener = new NsdManager.ResolveListener() {
-            @Override
-            public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
-                setEvent("onResolveFailed", errorCode);
-            }
-
-            @Override
-            public void onServiceResolved(NsdServiceInfo serviceInfo) {
-                mResolvedService = serviceInfo;
-                setEvent("onServiceResolved", serviceInfo);
-            }
-        };
-    }
-
-
-
-    private final class EventData {
-        EventData(String callbackName, NsdServiceInfo info) {
-            mCallbackName = callbackName;
-            mSucceeded = true;
-            mErrorCode = 0;
-            mInfo = info;
-        }
-        EventData(String callbackName, int errorCode) {
-            mCallbackName = callbackName;
-            mSucceeded = false;
-            mErrorCode = errorCode;
-            mInfo = null;
-        }
-        private final String mCallbackName;
-        private final boolean mSucceeded;
-        private final int mErrorCode;
-        private final NsdServiceInfo mInfo;
-    }
-
-    private final List<EventData> mEventCache = new ArrayList<EventData>();
-
-    private void setEvent(String callbackName, int errorCode) {
-        if (DBG) Log.d(TAG, callbackName + " failed with " + String.valueOf(errorCode));
-        EventData eventData = new EventData(callbackName, errorCode);
-        synchronized (mEventCache) {
-            mEventCache.add(eventData);
-            mEventCache.notify();
-        }
-    }
-
-    private void setEvent(String callbackName, NsdServiceInfo info) {
-        if (DBG) Log.d(TAG, "Received event " + callbackName + " for " + info.getServiceName());
-        EventData eventData = new EventData(callbackName, info);
-        synchronized (mEventCache) {
-            mEventCache.add(eventData);
-            mEventCache.notify();
-        }
-    }
-
-    void clearEventCache() {
-        synchronized(mEventCache) {
-            mEventCache.clear();
-        }
-    }
-
-    int eventCacheSize() {
-        synchronized(mEventCache) {
-            return mEventCache.size();
-        }
-    }
-
-    private int mWaitId = 0;
-    private EventData waitForCallback(String callbackName) {
-
-        synchronized(mEventCache) {
-
-            mWaitId ++;
-            if (DBG) Log.d(TAG, "Waiting for " + callbackName + ", id=" + String.valueOf(mWaitId));
-
-            try {
-                long startTime = android.os.SystemClock.uptimeMillis();
-                long elapsedTime = 0;
-                int index = 0;
-                while (elapsedTime < TIMEOUT ) {
-                    // first check if we've received that event
-                    for (; index < mEventCache.size(); index++) {
-                        EventData e = mEventCache.get(index);
-                        if (e.mCallbackName.equals(callbackName)) {
-                            if (DBG) Log.d(TAG, "exiting wait id=" + String.valueOf(mWaitId));
-                            return e;
-                        }
-                    }
-
-                    // Not yet received, just wait
-                    mEventCache.wait(TIMEOUT - elapsedTime);
-                    elapsedTime = android.os.SystemClock.uptimeMillis() - startTime;
-                }
-                // we exited the loop because of TIMEOUT; fail the call
-                if (DBG) Log.d(TAG, "timed out waiting id=" + String.valueOf(mWaitId));
-                return null;
-            } catch (InterruptedException e) {
-                return null;                       // wait timed out!
-            }
-        }
-    }
-
-    private EventData waitForNewEvents() throws InterruptedException {
-        if (DBG) Log.d(TAG, "Waiting for a bit, id=" + String.valueOf(mWaitId));
-
-        long startTime = android.os.SystemClock.uptimeMillis();
-        long elapsedTime = 0;
-        synchronized (mEventCache) {
-            int index = mEventCache.size();
-            while (elapsedTime < TIMEOUT ) {
-                // first check if we've received that event
-                for (; index < mEventCache.size(); index++) {
-                    EventData e = mEventCache.get(index);
-                    return e;
-                }
-
-                // Not yet received, just wait
-                mEventCache.wait(TIMEOUT - elapsedTime);
-                elapsedTime = android.os.SystemClock.uptimeMillis() - startTime;
-            }
-        }
-
-        return null;
-    }
-
-    private String mServiceName;
-
-    @Override
-    public void setUp() throws Exception {
-        super.setUp();
-        if (DBG) Log.d(TAG, "Setup test ...");
-        mNsdManager = (NsdManager) getContext().getSystemService(Context.NSD_SERVICE);
-
-        Random rand = new Random();
-        mServiceName = new String("NsdTest");
-        for (int i = 0; i < 4; i++) {
-            mServiceName = mServiceName + String.valueOf(rand.nextInt(10));
-        }
-    }
-
-    @Override
-    public void tearDown() throws Exception {
-        if (DBG) Log.d(TAG, "Tear down test ...");
-        super.tearDown();
-    }
-
-    public void testNDSManager() throws Exception {
-        EventData lastEvent = null;
-
-        if (DBG) Log.d(TAG, "Starting test ...");
-
-        NsdServiceInfo si = new NsdServiceInfo();
-        si.setServiceType(SERVICE_TYPE);
-        si.setServiceName(mServiceName);
-
-        byte testByteArray[] = new byte[] {-128, 127, 2, 1, 0, 1, 2};
-        String String256 = "1_________2_________3_________4_________5_________6_________" +
-                 "7_________8_________9_________10________11________12________13________" +
-                 "14________15________16________17________18________19________20________" +
-                 "21________22________23________24________25________123456";
-
-        // Illegal attributes
-        try {
-            si.setAttribute(null, (String) null);
-            fail("Could set null key");
-        } catch (IllegalArgumentException e) {
-            // expected
-        }
-
-        try {
-            si.setAttribute("", (String) null);
-            fail("Could set empty key");
-        } catch (IllegalArgumentException e) {
-            // expected
-        }
-
-        try {
-            si.setAttribute(String256, (String) null);
-            fail("Could set key with 255 characters");
-        } catch (IllegalArgumentException e) {
-            // expected
-        }
-
-        try {
-            si.setAttribute("key", String256.substring(3));
-            fail("Could set key+value combination with more than 255 characters");
-        } catch (IllegalArgumentException e) {
-            // expected
-        }
-
-        try {
-            si.setAttribute("key", String256.substring(4));
-            fail("Could set key+value combination with 255 characters");
-        } catch (IllegalArgumentException e) {
-            // expected
-        }
-
-        try {
-            si.setAttribute(new String(new byte[]{0x19}), (String) null);
-            fail("Could set key with invalid character");
-        } catch (IllegalArgumentException e) {
-            // expected
-        }
-
-        try {
-            si.setAttribute("=", (String) null);
-            fail("Could set key with invalid character");
-        } catch (IllegalArgumentException e) {
-            // expected
-        }
-
-        try {
-            si.setAttribute(new String(new byte[]{0x7F}), (String) null);
-            fail("Could set key with invalid character");
-        } catch (IllegalArgumentException e) {
-            // expected
-        }
-
-        // Allowed attributes
-        si.setAttribute("booleanAttr", (String) null);
-        si.setAttribute("keyValueAttr", "value");
-        si.setAttribute("keyEqualsAttr", "=");
-        si.setAttribute(" whiteSpaceKeyValueAttr ", " value ");
-        si.setAttribute("binaryDataAttr", testByteArray);
-        si.setAttribute("nullBinaryDataAttr", (byte[]) null);
-        si.setAttribute("emptyBinaryDataAttr", new byte[]{});
-        si.setAttribute("longkey", String256.substring(9));
-
-        ServerSocket socket;
-        int localPort;
-
-        try {
-            socket = new ServerSocket(0);
-            localPort = socket.getLocalPort();
-            si.setPort(localPort);
-        } catch (IOException e) {
-            if (DBG) Log.d(TAG, "Could not open a local socket");
-            assertTrue(false);
-            return;
-        }
-
-        if (DBG) Log.d(TAG, "Port = " + String.valueOf(localPort));
-
-        clearEventCache();
-
-        mNsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, mRegistrationListener);
-        lastEvent = waitForCallback("onServiceRegistered");                 // id = 1
-        assertTrue(lastEvent != null);
-        assertTrue(lastEvent.mSucceeded);
-        assertTrue(eventCacheSize() == 1);
-
-        // We may not always get the name that we tried to register;
-        // This events tells us the name that was registered.
-        String registeredName = lastEvent.mInfo.getServiceName();
-        si.setServiceName(registeredName);
-
-        clearEventCache();
-
-        mNsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD,
-                mDiscoveryListener);
-
-        // Expect discovery started
-        lastEvent = waitForCallback("onDiscoveryStarted");                  // id = 2
-
-        assertTrue(lastEvent != null);
-        assertTrue(lastEvent.mSucceeded);
-
-        // Remove this event, so accounting becomes easier later
-        synchronized (mEventCache) {
-            mEventCache.remove(lastEvent);
-        }
-
-        // Expect a service record to be discovered (and filter the ones
-        // that are unrelated to this test)
-        boolean found = false;
-        for (int i = 0; i < 32; i++) {
-
-            lastEvent = waitForCallback("onServiceFound");                  // id = 3
-            if (lastEvent == null) {
-                // no more onServiceFound events are being reported!
-                break;
-            }
-
-            assertTrue(lastEvent.mSucceeded);
-
-            if (DBG) Log.d(TAG, "id = " + String.valueOf(mWaitId) + ": ServiceName = " +
-                    lastEvent.mInfo.getServiceName());
-
-            if (lastEvent.mInfo.getServiceName().equals(registeredName)) {
-                // Save it, as it will get overwritten with new serviceFound events
-                si = lastEvent.mInfo;
-                found = true;
-            }
-
-            // Remove this event from the event cache, so it won't be found by subsequent
-            // calls to waitForCallback
-            synchronized (mEventCache) {
-                mEventCache.remove(lastEvent);
-            }
-        }
-
-        assertTrue(found);
-
-        // We've removed all serviceFound events, and we've removed the discoveryStarted
-        // event as well, so now the event cache should be empty!
-        assertTrue(eventCacheSize() == 0);
-
-        // Resolve the service
-        clearEventCache();
-        mNsdManager.resolveService(si, mResolveListener);
-        lastEvent = waitForCallback("onServiceResolved");                   // id = 4
-
-        assertNotNull(mResolvedService);
-
-        // Check Txt attributes
-        assertEquals(8, mResolvedService.getAttributes().size());
-        assertTrue(mResolvedService.getAttributes().containsKey("booleanAttr"));
-        assertNull(mResolvedService.getAttributes().get("booleanAttr"));
-        assertEquals("value", new String(mResolvedService.getAttributes().get("keyValueAttr")));
-        assertEquals("=", new String(mResolvedService.getAttributes().get("keyEqualsAttr")));
-        assertEquals(" value ", new String(mResolvedService.getAttributes()
-                .get(" whiteSpaceKeyValueAttr ")));
-        assertEquals(String256.substring(9), new String(mResolvedService.getAttributes()
-                .get("longkey")));
-        assertTrue(Arrays.equals(testByteArray,
-                mResolvedService.getAttributes().get("binaryDataAttr")));
-        assertTrue(mResolvedService.getAttributes().containsKey("nullBinaryDataAttr"));
-        assertNull(mResolvedService.getAttributes().get("nullBinaryDataAttr"));
-        assertTrue(mResolvedService.getAttributes().containsKey("emptyBinaryDataAttr"));
-        assertNull(mResolvedService.getAttributes().get("emptyBinaryDataAttr"));
-
-        assertTrue(lastEvent != null);
-        assertTrue(lastEvent.mSucceeded);
-
-        if (DBG) Log.d(TAG, "id = " + String.valueOf(mWaitId) + ": Port = " +
-                String.valueOf(lastEvent.mInfo.getPort()));
-
-        assertTrue(lastEvent.mInfo.getPort() == localPort);
-        assertTrue(eventCacheSize() == 1);
-
-        checkForAdditionalEvents();
-        clearEventCache();
-
-        // Unregister the service
-        mNsdManager.unregisterService(mRegistrationListener);
-        lastEvent = waitForCallback("onServiceUnregistered");               // id = 5
-
-        assertTrue(lastEvent != null);
-        assertTrue(lastEvent.mSucceeded);
-
-        // Expect a callback for service lost
-        lastEvent = waitForCallback("onServiceLost");                       // id = 6
-
-        assertTrue(lastEvent != null);
-        assertTrue(lastEvent.mInfo.getServiceName().equals(registeredName));
-
-        // Register service again to see if we discover it
-        checkForAdditionalEvents();
-        clearEventCache();
-
-        si = new NsdServiceInfo();
-        si.setServiceType(SERVICE_TYPE);
-        si.setServiceName(mServiceName);
-        si.setPort(localPort);
-
-        // Create a new registration listener and register same service again
-        initRegistrationListener();
-
-        mNsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, mRegistrationListener);
-
-        lastEvent = waitForCallback("onServiceRegistered");                 // id = 7
-
-        assertTrue(lastEvent != null);
-        assertTrue(lastEvent.mSucceeded);
-
-        registeredName = lastEvent.mInfo.getServiceName();
-
-        // Expect a record to be discovered
-        // Expect a service record to be discovered (and filter the ones
-        // that are unrelated to this test)
-        found = false;
-        for (int i = 0; i < 32; i++) {
-
-            lastEvent = waitForCallback("onServiceFound");                  // id = 8
-            if (lastEvent == null) {
-                // no more onServiceFound events are being reported!
-                break;
-            }
-
-            assertTrue(lastEvent.mSucceeded);
-
-            if (DBG) Log.d(TAG, "id = " + String.valueOf(mWaitId) + ": ServiceName = " +
-                    lastEvent.mInfo.getServiceName());
-
-            if (lastEvent.mInfo.getServiceName().equals(registeredName)) {
-                // Save it, as it will get overwritten with new serviceFound events
-                si = lastEvent.mInfo;
-                found = true;
-            }
-
-            // Remove this event from the event cache, so it won't be found by subsequent
-            // calls to waitForCallback
-            synchronized (mEventCache) {
-                mEventCache.remove(lastEvent);
-            }
-        }
-
-        assertTrue(found);
-
-        // Resolve the service
-        clearEventCache();
-        mNsdManager.resolveService(si, mResolveListener);
-        lastEvent = waitForCallback("onServiceResolved");                   // id = 9
-
-        assertTrue(lastEvent != null);
-        assertTrue(lastEvent.mSucceeded);
-
-        if (DBG) Log.d(TAG, "id = " + String.valueOf(mWaitId) + ": ServiceName = " +
-                lastEvent.mInfo.getServiceName());
-
-        assertTrue(lastEvent.mInfo.getServiceName().equals(registeredName));
-
-        assertNotNull(mResolvedService);
-
-        // Check that we don't have any TXT records
-        assertEquals(0, mResolvedService.getAttributes().size());
-
-        checkForAdditionalEvents();
-        clearEventCache();
-
-        mNsdManager.stopServiceDiscovery(mDiscoveryListener);
-        lastEvent = waitForCallback("onDiscoveryStopped");                  // id = 10
-        assertTrue(lastEvent != null);
-        assertTrue(lastEvent.mSucceeded);
-        assertTrue(checkCacheSize(1));
-
-        checkForAdditionalEvents();
-        clearEventCache();
-
-        mNsdManager.unregisterService(mRegistrationListener);
-
-        lastEvent =  waitForCallback("onServiceUnregistered");              // id = 11
-        assertTrue(lastEvent != null);
-        assertTrue(lastEvent.mSucceeded);
-        assertTrue(checkCacheSize(1));
-    }
-
-    boolean checkCacheSize(int size) {
-        synchronized (mEventCache) {
-            int cacheSize = mEventCache.size();
-            if (cacheSize != size) {
-                Log.d(TAG, "id = " + mWaitId + ": event cache size = " + cacheSize);
-                for (int i = 0; i < cacheSize; i++) {
-                    EventData e = mEventCache.get(i);
-                    String sname = (e.mInfo != null) ? "(" + e.mInfo.getServiceName() + ")" : "";
-                    Log.d(TAG, "eventName is " + e.mCallbackName + sname);
-                }
-            }
-            return (cacheSize == size);
-        }
-    }
-
-    boolean checkForAdditionalEvents() {
-        try {
-            EventData e = waitForNewEvents();
-            if (e != null) {
-                String sname = (e.mInfo != null) ? "(" + e.mInfo.getServiceName() + ")" : "";
-                Log.d(TAG, "ignoring unexpected event " + e.mCallbackName + sname);
-            }
-            return (e == null);
-        }
-        catch (InterruptedException ex) {
-            return false;
-        }
-    }
-}
-
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
new file mode 100644
index 0000000..9307c27
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -0,0 +1,325 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.cts
+
+import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStarted
+import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped
+import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.ServiceFound
+import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.ServiceLost
+import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.StartDiscoveryFailed
+import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.StopDiscoveryFailed
+import android.net.cts.NsdManagerTest.NsdRegistrationRecord.RegistrationEvent.RegistrationFailed
+import android.net.cts.NsdManagerTest.NsdRegistrationRecord.RegistrationEvent.ServiceRegistered
+import android.net.cts.NsdManagerTest.NsdRegistrationRecord.RegistrationEvent.ServiceUnregistered
+import android.net.cts.NsdManagerTest.NsdRegistrationRecord.RegistrationEvent.UnregistrationFailed
+import android.net.cts.NsdManagerTest.NsdResolveRecord.ResolveEvent.ResolveFailed
+import android.net.cts.NsdManagerTest.NsdResolveRecord.ResolveEvent.ServiceResolved
+import android.net.nsd.NsdManager
+import android.net.nsd.NsdManager.DiscoveryListener
+import android.net.nsd.NsdManager.RegistrationListener
+import android.net.nsd.NsdManager.ResolveListener
+import android.net.nsd.NsdServiceInfo
+import android.platform.test.annotations.AppModeFull
+import android.util.Log
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.runner.AndroidJUnit4
+import com.android.net.module.util.ArrayTrackRecord
+import com.android.net.module.util.TrackRecord
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.net.ServerSocket
+import java.nio.charset.StandardCharsets
+import java.util.Random
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+private const val TAG = "NsdManagerTest"
+private const val SERVICE_TYPE = "_nmt._tcp"
+private const val TIMEOUT_MS = 2000L
+private const val DBG = false
+
+@AppModeFull(reason = "Socket cannot bind in instant app mode")
+@RunWith(AndroidJUnit4::class)
+class NsdManagerTest {
+    private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
+    private val nsdManager by lazy { context.getSystemService(NsdManager::class.java) }
+    private val serviceName = "NsdTest%04d".format(Random().nextInt(1000))
+
+    private interface NsdEvent
+    private open class NsdRecord<T : NsdEvent> private constructor(
+        private val history: ArrayTrackRecord<T>
+    ) : TrackRecord<T> by history {
+        constructor() : this(ArrayTrackRecord())
+
+        val nextEvents = history.newReadHead()
+
+        inline fun <reified V : NsdEvent> expectCallbackEventually(
+            crossinline predicate: (V) -> Boolean = { true }
+        ): V = nextEvents.poll(TIMEOUT_MS) { e -> e is V && predicate(e) } as V?
+                ?: fail("Callback for ${V::class.java.simpleName} not seen after $TIMEOUT_MS ms")
+
+        inline fun <reified V : NsdEvent> expectCallback(): V {
+            val nextEvent = nextEvents.poll(TIMEOUT_MS)
+            assertNotNull(nextEvent, "No callback received after $TIMEOUT_MS ms")
+            assertTrue(nextEvent is V, "Expected ${V::class.java.simpleName} but got " +
+                    nextEvent.javaClass.simpleName)
+            return nextEvent
+        }
+    }
+
+    private class NsdRegistrationRecord : RegistrationListener,
+            NsdRecord<NsdRegistrationRecord.RegistrationEvent>() {
+        sealed class RegistrationEvent : NsdEvent {
+            abstract val serviceInfo: NsdServiceInfo
+
+            data class RegistrationFailed(
+                override val serviceInfo: NsdServiceInfo,
+                val errorCode: Int
+            ) : RegistrationEvent()
+
+            data class UnregistrationFailed(
+                override val serviceInfo: NsdServiceInfo,
+                val errorCode: Int
+            ) : RegistrationEvent()
+
+            data class ServiceRegistered(override val serviceInfo: NsdServiceInfo)
+                : RegistrationEvent()
+            data class ServiceUnregistered(override val serviceInfo: NsdServiceInfo)
+                : RegistrationEvent()
+        }
+
+        override fun onRegistrationFailed(si: NsdServiceInfo, err: Int) {
+            add(RegistrationFailed(si, err))
+        }
+
+        override fun onUnregistrationFailed(si: NsdServiceInfo, err: Int) {
+            add(UnregistrationFailed(si, err))
+        }
+
+        override fun onServiceRegistered(si: NsdServiceInfo) {
+            add(ServiceRegistered(si))
+        }
+
+        override fun onServiceUnregistered(si: NsdServiceInfo) {
+            add(ServiceUnregistered(si))
+        }
+    }
+
+    private class NsdDiscoveryRecord : DiscoveryListener,
+            NsdRecord<NsdDiscoveryRecord.DiscoveryEvent>() {
+        sealed class DiscoveryEvent : NsdEvent {
+            data class StartDiscoveryFailed(val serviceType: String, val errorCode: Int)
+                : DiscoveryEvent()
+
+            data class StopDiscoveryFailed(val serviceType: String, val errorCode: Int)
+                : DiscoveryEvent()
+
+            data class DiscoveryStarted(val serviceType: String) : DiscoveryEvent()
+            data class DiscoveryStopped(val serviceType: String) : DiscoveryEvent()
+            data class ServiceFound(val serviceInfo: NsdServiceInfo) : DiscoveryEvent()
+            data class ServiceLost(val serviceInfo: NsdServiceInfo) : DiscoveryEvent()
+        }
+
+        override fun onStartDiscoveryFailed(serviceType: String, err: Int) {
+            add(StartDiscoveryFailed(serviceType, err))
+        }
+
+        override fun onStopDiscoveryFailed(serviceType: String, err: Int) {
+            add(StopDiscoveryFailed(serviceType, err))
+        }
+
+        override fun onDiscoveryStarted(serviceType: String) {
+            add(DiscoveryStarted(serviceType))
+        }
+
+        override fun onDiscoveryStopped(serviceType: String) {
+            add(DiscoveryStopped(serviceType))
+        }
+
+        override fun onServiceFound(si: NsdServiceInfo) {
+            add(ServiceFound(si))
+        }
+
+        override fun onServiceLost(si: NsdServiceInfo) {
+            add(ServiceLost(si))
+        }
+
+        fun waitForServiceDiscovered(serviceName: String): NsdServiceInfo {
+            return expectCallbackEventually<ServiceFound> {
+                it.serviceInfo.serviceName == serviceName
+            }.serviceInfo
+        }
+    }
+
+    private class NsdResolveRecord : ResolveListener,
+            NsdRecord<NsdResolveRecord.ResolveEvent>() {
+        sealed class ResolveEvent : NsdEvent {
+            data class ResolveFailed(val serviceInfo: NsdServiceInfo, val errorCode: Int)
+                : ResolveEvent()
+
+            data class ServiceResolved(val serviceInfo: NsdServiceInfo) : ResolveEvent()
+        }
+
+        override fun onResolveFailed(si: NsdServiceInfo, err: Int) {
+            add(ResolveFailed(si, err))
+        }
+
+        override fun onServiceResolved(si: NsdServiceInfo) {
+            add(ServiceResolved(si))
+        }
+    }
+
+    @Test
+    fun testNsdManager() {
+        val si = NsdServiceInfo()
+        si.serviceType = SERVICE_TYPE
+        si.serviceName = serviceName
+        // Test binary data with various bytes
+        val testByteArray = byteArrayOf(-128, 127, 2, 1, 0, 1, 2)
+        // Test string data with 256 characters (25 blocks of 10 characters + 6)
+        val string256 = "1_________2_________3_________4_________5_________6_________" +
+                "7_________8_________9_________10________11________12________13________" +
+                "14________15________16________17________18________19________20________" +
+                "21________22________23________24________25________123456"
+
+        // Illegal attributes
+        listOf(
+                Triple(null, null, "null key"),
+                Triple("", null, "empty key"),
+                Triple(string256, null, "key with 256 characters"),
+                Triple("key", string256.substring(3),
+                        "key+value combination with more than 255 characters"),
+                Triple("key", string256.substring(4), "key+value combination with 255 characters"),
+                Triple("\u0019", null, "key with invalid character"),
+                Triple("=", null, "key with invalid character"),
+                Triple("\u007f", null, "key with invalid character")
+        ).forEach {
+            assertFailsWith<IllegalArgumentException>(
+                    "Setting invalid ${it.third} unexpectedly succeeded") {
+                si.setAttribute(it.first, it.second)
+            }
+        }
+
+        // Allowed attributes
+        si.setAttribute("booleanAttr", null as String?)
+        si.setAttribute("keyValueAttr", "value")
+        si.setAttribute("keyEqualsAttr", "=")
+        si.setAttribute(" whiteSpaceKeyValueAttr ", " value ")
+        si.setAttribute("binaryDataAttr", testByteArray)
+        si.setAttribute("nullBinaryDataAttr", null as ByteArray?)
+        si.setAttribute("emptyBinaryDataAttr", byteArrayOf())
+        si.setAttribute("longkey", string256.substring(9))
+        val socket = ServerSocket(0)
+        val localPort = socket.localPort
+        si.port = localPort
+        if (DBG) Log.d(TAG, "Port = $localPort")
+
+        val registrationRecord = NsdRegistrationRecord()
+        val registeredInfo = registerService(registrationRecord, si)
+
+        val discoveryRecord = NsdDiscoveryRecord()
+        nsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryRecord)
+
+        // Expect discovery started
+        discoveryRecord.expectCallback<DiscoveryStarted>()
+
+        // Expect a service record to be discovered
+        val foundInfo = discoveryRecord.waitForServiceDiscovered(registeredInfo.serviceName)
+
+        val resolvedService = resolveService(foundInfo)
+
+        // Check Txt attributes
+        assertEquals(8, resolvedService.attributes.size)
+        assertTrue(resolvedService.attributes.containsKey("booleanAttr"))
+        assertNull(resolvedService.attributes["booleanAttr"])
+        assertEquals("value", resolvedService.attributes["keyValueAttr"].utf8ToString())
+        assertEquals("=", resolvedService.attributes["keyEqualsAttr"].utf8ToString())
+        assertEquals(" value ",
+                resolvedService.attributes[" whiteSpaceKeyValueAttr "].utf8ToString())
+        assertEquals(string256.substring(9), resolvedService.attributes["longkey"].utf8ToString())
+        assertArrayEquals(testByteArray, resolvedService.attributes["binaryDataAttr"])
+        assertTrue(resolvedService.attributes.containsKey("nullBinaryDataAttr"))
+        assertNull(resolvedService.attributes["nullBinaryDataAttr"])
+        assertTrue(resolvedService.attributes.containsKey("emptyBinaryDataAttr"))
+        assertNull(resolvedService.attributes["emptyBinaryDataAttr"])
+        assertEquals(localPort, resolvedService.port)
+
+        // Unregister the service
+        nsdManager.unregisterService(registrationRecord)
+        registrationRecord.expectCallback<ServiceUnregistered>()
+
+        // Expect a callback for service lost
+        discoveryRecord.expectCallbackEventually<ServiceLost> {
+            it.serviceInfo.serviceName == serviceName
+        }
+
+        // Register service again to see if NsdManager can discover it
+        val si2 = NsdServiceInfo()
+        si2.serviceType = SERVICE_TYPE
+        si2.serviceName = serviceName
+        si2.port = localPort
+        val registrationRecord2 = NsdRegistrationRecord()
+        val registeredInfo2 = registerService(registrationRecord2, si2)
+
+        // Expect a service record to be discovered (and filter the ones
+        // that are unrelated to this test)
+        val foundInfo2 = discoveryRecord.waitForServiceDiscovered(registeredInfo2.serviceName)
+
+        // Resolve the service
+        val resolvedService2 = resolveService(foundInfo2)
+
+        // Check that the resolved service doesn't have any TXT records
+        assertEquals(0, resolvedService2.attributes.size)
+
+        nsdManager.stopServiceDiscovery(discoveryRecord)
+
+        discoveryRecord.expectCallbackEventually<DiscoveryStopped>()
+
+        nsdManager.unregisterService(registrationRecord2)
+        registrationRecord2.expectCallback<ServiceUnregistered>()
+    }
+
+    /**
+     * Register a service and return its registration record.
+     */
+    private fun registerService(record: NsdRegistrationRecord, si: NsdServiceInfo): NsdServiceInfo {
+        nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, record)
+        // We may not always get the name that we tried to register;
+        // This events tells us the name that was registered.
+        val cb = record.expectCallback<ServiceRegistered>()
+        return cb.serviceInfo
+    }
+
+    private fun resolveService(discoveredInfo: NsdServiceInfo): NsdServiceInfo {
+        val record = NsdResolveRecord()
+        nsdManager.resolveService(discoveredInfo, record)
+        val resolvedCb = record.expectCallback<ServiceResolved>()
+        assertEquals(discoveredInfo.serviceName, resolvedCb.serviceInfo.serviceName)
+
+        return resolvedCb.serviceInfo
+    }
+}
+
+private fun ByteArray?.utf8ToString(): String {
+    if (this == null) return ""
+    return String(this, StandardCharsets.UTF_8)
+}
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index c47604c..8d91552 100644
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -3386,12 +3386,12 @@
 
     private NativeNetworkConfig nativeNetworkConfigPhysical(int netId, int permission) {
         return new NativeNetworkConfig(netId, NativeNetworkType.PHYSICAL, permission,
-                /*secure=*/ false, VpnManager.TYPE_VPN_NONE);
+                /*secure=*/ false, VpnManager.TYPE_VPN_NONE, /*excludeLocalRoutes=*/ false);
     }
 
     private NativeNetworkConfig nativeNetworkConfigVpn(int netId, boolean secure, int vpnType) {
         return new NativeNetworkConfig(netId, NativeNetworkType.VIRTUAL, INetd.PERMISSION_NONE,
-                secure, vpnType);
+                secure, vpnType, /*excludeLocalRoutes=*/ false);
     }
 
     @Test