Merge "Test Data Stall with unknown detection type." into rvc-dev
diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
index a820ae5..5aafdf0 100644
--- a/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
+++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
@@ -25,4 +25,5 @@
     String getRestrictBackgroundStatus();
     void sendNotification(int notificationId, String notificationType);
     void registerNetworkCallback(in INetworkCallback cb);
+    void unregisterNetworkCallback();
 }
diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkCallback.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkCallback.aidl
index 740ec26..2048bab 100644
--- a/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkCallback.aidl
+++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkCallback.aidl
@@ -17,9 +17,11 @@
 package com.android.cts.net.hostside;
 
 import android.net.Network;
+import android.net.NetworkCapabilities;
 
 interface INetworkCallback {
     void onBlockedStatusChanged(in Network network, boolean blocked);
     void onAvailable(in Network network);
     void onLost(in Network network);
+    void onCapabilitiesChanged(in Network network, in NetworkCapabilities cap);
 }
diff --git a/tests/cts/hostside/app/AndroidManifest.xml b/tests/cts/hostside/app/AndroidManifest.xml
index 5ddad7c..3940de4 100644
--- a/tests/cts/hostside/app/AndroidManifest.xml
+++ b/tests/cts/hostside/app/AndroidManifest.xml
@@ -28,6 +28,7 @@
     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
     <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
 
     <application android:requestLegacyExternalStorage="true" >
         <uses-library android:name="android.test.runner" />
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractAppIdleTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractAppIdleTestCase.java
index 51bdf8e..5fe4573 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractAppIdleTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractAppIdleTestCase.java
@@ -50,7 +50,7 @@
     public final void tearDown() throws Exception {
         super.tearDown();
 
-        turnBatteryOff();
+        executeSilentShellCommand("cmd battery reset");
         setAppIdle(false);
     }
 
@@ -131,11 +131,11 @@
     @RequiredProperties({BATTERY_SAVER_MODE})
     @Test
     public void testAppIdleNetworkAccess_whenCharging() throws Exception {
-        // Check that idle app doesn't get network when charging
+        // Check that app is paroled when charging
         setAppIdle(true);
         assertBackgroundNetworkAccess(false);
         turnBatteryOff();
-        assertBackgroundNetworkAccess(false);
+        assertBackgroundNetworkAccess(true);
         turnBatteryOn();
         assertBackgroundNetworkAccess(false);
 
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 29ba68c..2072db3 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
@@ -30,6 +30,7 @@
 import static com.android.cts.net.hostside.NetworkPolicyTestUtils.restrictBackgroundValueToString;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -177,7 +178,9 @@
         do {
             attempts++;
             count = getNumberBroadcastsReceived(receiverName, ACTION_RESTRICT_BACKGROUND_CHANGED);
-            if (count >= expectedCount) {
+            assertFalse("Expected count " + expectedCount + " but actual is " + count,
+                    count > expectedCount);
+            if (count == expectedCount) {
                 break;
             }
             Log.d(TAG, "Expecting count " + expectedCount + " but actual is " + count + " after "
@@ -697,6 +700,10 @@
         mServiceClient.registerNetworkCallback(cb);
     }
 
+    protected void unregisterNetworkCallback() throws Exception {
+        mServiceClient.unregisterNetworkCallback();
+    }
+
     /**
      * Registers a {@link NotificationListenerService} implementation that will execute the
      * notification actions right after the notification is sent.
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
index 3ee7b99..6546e26 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
@@ -100,4 +100,8 @@
     public void registerNetworkCallback(INetworkCallback cb) throws RemoteException {
         mService.registerNetworkCallback(cb);
     }
+
+    public void unregisterNetworkCallback() throws RemoteException {
+        mService.unregisterNetworkCallback();
+    }
 }
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
index ed397b9..f3cd8a9 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
@@ -16,18 +16,23 @@
 
 package com.android.cts.net.hostside;
 
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
 import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground;
 import static com.android.cts.net.hostside.Property.BATTERY_SAVER_MODE;
 import static com.android.cts.net.hostside.Property.DATA_SAVER_MODE;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import android.net.Network;
+import android.net.NetworkCapabilities;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 
 import java.util.Objects;
@@ -35,15 +40,18 @@
 import java.util.concurrent.TimeUnit;
 
 public class NetworkCallbackTest extends AbstractRestrictBackgroundNetworkTestCase {
-
     private Network mNetwork;
     private final TestNetworkCallback mTestNetworkCallback = new TestNetworkCallback();
+    @Rule
+    public final MeterednessConfigurationRule mMeterednessConfiguration
+            = new MeterednessConfigurationRule();
 
     enum CallbackState {
         NONE,
         AVAILABLE,
         LOST,
-        BLOCKED_STATUS
+        BLOCKED_STATUS,
+        CAPABILITIES
     }
 
     private static class CallbackInfo {
@@ -75,7 +83,7 @@
     }
 
     private class TestNetworkCallback extends INetworkCallback.Stub {
-        private static final int TEST_CALLBACK_TIMEOUT_MS = 200;
+        private static final int TEST_CALLBACK_TIMEOUT_MS = 5_000;
 
         private final LinkedBlockingQueue<CallbackInfo> mCallbacks = new LinkedBlockingQueue<>();
 
@@ -117,12 +125,18 @@
             setLastCallback(CallbackState.BLOCKED_STATUS, network, blocked);
         }
 
-        public void expectLostCallback(Network expectedNetwork) {
-            expectCallback(CallbackState.LOST, expectedNetwork, null);
+        @Override
+        public void onCapabilitiesChanged(Network network, NetworkCapabilities cap) {
+            setLastCallback(CallbackState.CAPABILITIES, network, cap);
         }
 
-        public void expectAvailableCallback(Network expectedNetwork) {
-            expectCallback(CallbackState.AVAILABLE, expectedNetwork, null);
+        public Network expectAvailableCallbackAndGetNetwork() {
+            final CallbackInfo cb = nextCallback(TEST_CALLBACK_TIMEOUT_MS);
+            if (cb.state != CallbackState.AVAILABLE) {
+                fail("Network is not available. Instead obtained the following callback :"
+                        + cb);
+            }
+            return cb.network;
         }
 
         public void expectBlockedStatusCallback(Network expectedNetwork, boolean expectBlocked) {
@@ -130,15 +144,28 @@
                     expectBlocked);
         }
 
-        void assertNoCallback() {
-            CallbackInfo cb = null;
-            try {
-                cb = mCallbacks.poll(TEST_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
-            } catch (InterruptedException e) {
-                // Expected.
-            }
-            if (cb != null) {
-                assertNull("Unexpected callback: " + cb, cb);
+        public void waitBlockedStatusCallback(Network expectedNetwork, boolean expectBlocked) {
+            final long deadline = System.currentTimeMillis() + TEST_CALLBACK_TIMEOUT_MS;
+            do {
+                final CallbackInfo cb = nextCallback((int) (deadline - System.currentTimeMillis()));
+                if (cb.state == CallbackState.BLOCKED_STATUS) {
+                    assertEquals(expectBlocked, cb.arg);
+                    return;
+                }
+            } while (System.currentTimeMillis() <= deadline);
+            fail("Didn't receive onBlockedStatusChanged()");
+        }
+
+        public void expectCapabilitiesCallback(Network expectedNetwork, boolean hasCapability,
+                int capability) {
+            final CallbackInfo cb = nextCallback(TEST_CALLBACK_TIMEOUT_MS);
+            final NetworkCapabilities cap = (NetworkCapabilities) cb.arg;
+            assertEquals(expectedNetwork, cb.network);
+            assertEquals(CallbackState.CAPABILITIES, cb.state);
+            if (hasCapability != cap.hasCapability(capability)) {
+                fail("NetworkCapabilities callback "
+                        + (hasCapability ? "missing expected" : "has unexpected")
+                        + " capability. " + cb);
             }
         }
     }
@@ -147,13 +174,28 @@
     public void setUp() throws Exception {
         super.setUp();
 
-        mNetwork = mCm.getActiveNetwork();
-
         registerBroadcastReceiver();
 
         removeRestrictBackgroundWhitelist(mUid);
         removeRestrictBackgroundBlacklist(mUid);
         assertRestrictBackgroundChangedReceived(0);
+
+        // Initial state
+        setBatterySaverMode(false);
+        setRestrictBackground(false);
+
+        // Make wifi a metered network.
+        mMeterednessConfiguration.configureNetworkMeteredness(true);
+
+        // Register callback
+        registerNetworkCallback((INetworkCallback.Stub) mTestNetworkCallback);
+        // Once the wifi is marked as metered, the wifi will reconnect. Wait for onAvailable()
+        // callback to ensure wifi is connected before the test and store the default network.
+        mNetwork = mTestNetworkCallback.expectAvailableCallbackAndGetNetwork();
+        // Check that the network is metered.
+        mTestNetworkCallback.expectCapabilitiesCallback(mNetwork, false /* hasCapability */,
+                NET_CAPABILITY_NOT_METERED);
+        mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, false);
     }
 
     @After
@@ -162,102 +204,80 @@
 
         setRestrictBackground(false);
         setBatterySaverMode(false);
+        unregisterNetworkCallback();
     }
 
     @RequiredProperties({DATA_SAVER_MODE})
     @Test
     public void testOnBlockedStatusChanged_dataSaver() throws Exception {
-        // Initial state
-        setBatterySaverMode(false);
-        setRestrictBackground(false);
-
-        final MeterednessConfigurationRule meterednessConfiguration
-                = new MeterednessConfigurationRule();
-        meterednessConfiguration.configureNetworkMeteredness(true);
         try {
-            // Register callback
-            registerNetworkCallback((INetworkCallback.Stub) mTestNetworkCallback);
-            mTestNetworkCallback.expectAvailableCallback(mNetwork);
-            mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, false);
-
             // Enable restrict background
             setRestrictBackground(true);
             assertBackgroundNetworkAccess(false);
-            mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, true);
+            mTestNetworkCallback.waitBlockedStatusCallback(mNetwork, true);
 
             // Add to whitelist
             addRestrictBackgroundWhitelist(mUid);
             assertBackgroundNetworkAccess(true);
-            mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, false);
+            mTestNetworkCallback.waitBlockedStatusCallback(mNetwork, false);
 
             // Remove from whitelist
             removeRestrictBackgroundWhitelist(mUid);
             assertBackgroundNetworkAccess(false);
-            mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, true);
+            mTestNetworkCallback.waitBlockedStatusCallback(mNetwork, true);
         } finally {
-            meterednessConfiguration.resetNetworkMeteredness();
+            mMeterednessConfiguration.resetNetworkMeteredness();
         }
 
         // Set to non-metered network
-        meterednessConfiguration.configureNetworkMeteredness(false);
+        mMeterednessConfiguration.configureNetworkMeteredness(false);
+        mTestNetworkCallback.expectCapabilitiesCallback(mNetwork, true /* hasCapability */,
+                NET_CAPABILITY_NOT_METERED);
         try {
             assertBackgroundNetworkAccess(true);
-            mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, false);
+            mTestNetworkCallback.waitBlockedStatusCallback(mNetwork, false);
 
             // Disable restrict background, should not trigger callback
             setRestrictBackground(false);
             assertBackgroundNetworkAccess(true);
-            mTestNetworkCallback.assertNoCallback();
         } finally {
-            meterednessConfiguration.resetNetworkMeteredness();
+            mMeterednessConfiguration.resetNetworkMeteredness();
         }
     }
 
     @RequiredProperties({BATTERY_SAVER_MODE})
     @Test
     public void testOnBlockedStatusChanged_powerSaver() throws Exception {
-        // Set initial state.
-        setBatterySaverMode(false);
-        setRestrictBackground(false);
-
-        final MeterednessConfigurationRule meterednessConfiguration
-                = new MeterednessConfigurationRule();
-        meterednessConfiguration.configureNetworkMeteredness(true);
         try {
-            // Register callback
-            registerNetworkCallback((INetworkCallback.Stub) mTestNetworkCallback);
-            mTestNetworkCallback.expectAvailableCallback(mNetwork);
-            mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, false);
-
             // Enable Power Saver
             setBatterySaverMode(true);
             assertBackgroundNetworkAccess(false);
-            mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, true);
+            mTestNetworkCallback.waitBlockedStatusCallback(mNetwork, true);
 
             // Disable Power Saver
             setBatterySaverMode(false);
             assertBackgroundNetworkAccess(true);
-            mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, false);
+            mTestNetworkCallback.waitBlockedStatusCallback(mNetwork, false);
         } finally {
-            meterednessConfiguration.resetNetworkMeteredness();
+            mMeterednessConfiguration.resetNetworkMeteredness();
         }
 
         // Set to non-metered network
-        meterednessConfiguration.configureNetworkMeteredness(false);
+        mMeterednessConfiguration.configureNetworkMeteredness(false);
+        mTestNetworkCallback.expectCapabilitiesCallback(mNetwork, true /* hasCapability */,
+                NET_CAPABILITY_NOT_METERED);
         try {
-            mTestNetworkCallback.assertNoCallback();
-
             // Enable Power Saver
             setBatterySaverMode(true);
             assertBackgroundNetworkAccess(false);
-            mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, true);
+            mTestNetworkCallback.waitBlockedStatusCallback(mNetwork, true);
 
             // Disable Power Saver
             setBatterySaverMode(false);
             assertBackgroundNetworkAccess(true);
-            mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, false);
+            mTestNetworkCallback.waitBlockedStatusCallback(mNetwork, false);
         } finally {
-            meterednessConfiguration.resetNetworkMeteredness();
+            mMeterednessConfiguration.resetNetworkMeteredness();
         }
     }
 
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
index ec536af..590e17e 100644
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
@@ -127,6 +127,16 @@
                         unregisterNetworkCallback();
                     }
                 }
+
+                @Override
+                public void onCapabilitiesChanged(Network network, NetworkCapabilities cap) {
+                    try {
+                        cb.onCapabilitiesChanged(network, cap);
+                    } catch (RemoteException e) {
+                        Log.d(TAG, "Cannot send onCapabilitiesChanged: " + e);
+                        unregisterNetworkCallback();
+                    }
+                }
             };
             mCm.registerNetworkCallback(makeWifiNetworkRequest(), mNetworkCallback);
             try {
@@ -135,17 +145,21 @@
                 unregisterNetworkCallback();
             }
         }
-      };
 
-    private void unregisterNetworkCallback() {
-        Log.d(TAG, "unregistering network callback");
-        mCm.unregisterNetworkCallback(mNetworkCallback);
-        mNetworkCallback = null;
-    }
+        @Override
+        public void unregisterNetworkCallback() {
+            Log.d(TAG, "unregistering network callback");
+            if (mNetworkCallback != null) {
+                mCm.unregisterNetworkCallback(mNetworkCallback);
+                mNetworkCallback = null;
+            }
+        }
+      };
 
     private NetworkRequest makeWifiNetworkRequest() {
         return new NetworkRequest.Builder()
                 .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                 .build();
     }
 
diff --git a/tests/cts/hostside/src/com/android/cts/net/NetworkPolicyTestsPreparer.java b/tests/cts/hostside/src/com/android/cts/net/NetworkPolicyTestsPreparer.java
index dfce7da..23aca24 100644
--- a/tests/cts/hostside/src/com/android/cts/net/NetworkPolicyTestsPreparer.java
+++ b/tests/cts/hostside/src/com/android/cts/net/NetworkPolicyTestsPreparer.java
@@ -23,7 +23,11 @@
 
 public class NetworkPolicyTestsPreparer implements ITargetPreparer {
     private ITestDevice mDevice;
+    private boolean mOriginalAirplaneModeEnabled;
     private String mOriginalAppStandbyEnabled;
+    private String mOriginalBatteryStatsConstants;
+    private final static String KEY_STABLE_CHARGING_DELAY_MS = "battery_charged_delay_ms";
+    private final static int DESIRED_STABLE_CHARGING_DELAY_MS = 0;
 
     @Override
     public void setUp(TestInformation testInformation) throws DeviceNotAvailableException {
@@ -31,12 +35,32 @@
         mOriginalAppStandbyEnabled = getAppStandbyEnabled();
         setAppStandbyEnabled("1");
         LogUtil.CLog.d("Original app_standby_enabled: " + mOriginalAppStandbyEnabled);
+
+        mOriginalBatteryStatsConstants = getBatteryStatsConstants();
+        setBatteryStatsConstants(
+                KEY_STABLE_CHARGING_DELAY_MS + "=" + DESIRED_STABLE_CHARGING_DELAY_MS);
+        LogUtil.CLog.d("Original battery_saver_constants: " + mOriginalBatteryStatsConstants);
+
+        mOriginalAirplaneModeEnabled = getAirplaneModeEnabled();
+        // Turn off airplane mode in case another test left the device in that state.
+        setAirplaneModeEnabled(false);
+        LogUtil.CLog.d("Original airplane mode state: " + mOriginalAirplaneModeEnabled);
     }
 
     @Override
     public void tearDown(TestInformation testInformation, Throwable e)
             throws DeviceNotAvailableException {
+        setAirplaneModeEnabled(mOriginalAirplaneModeEnabled);
         setAppStandbyEnabled(mOriginalAppStandbyEnabled);
+        setBatteryStatsConstants(mOriginalBatteryStatsConstants);
+    }
+
+    private void setAirplaneModeEnabled(boolean enable) throws DeviceNotAvailableException {
+        executeCmd("cmd connectivity airplane-mode " + (enable ? "enable" : "disable"));
+    }
+
+    private boolean getAirplaneModeEnabled() throws DeviceNotAvailableException {
+        return "enabled".equals(executeCmd("cmd connectivity airplane-mode").trim());
     }
 
     private void setAppStandbyEnabled(String appStandbyEnabled) throws DeviceNotAvailableException {
@@ -51,6 +75,15 @@
         return executeCmd("settings get global app_standby_enabled").trim();
     }
 
+    private void setBatteryStatsConstants(String batteryStatsConstants)
+            throws DeviceNotAvailableException {
+        executeCmd("settings put global battery_stats_constants \"" + batteryStatsConstants + "\"");
+    }
+
+    private String getBatteryStatsConstants() throws DeviceNotAvailableException {
+        return executeCmd("settings get global battery_stats_constants");
+    }
+
     private String executeCmd(String cmd) throws DeviceNotAvailableException {
         final String output = mDevice.executeShellCommand(cmd).trim();
         LogUtil.CLog.d("Output for '%s': %s", cmd, output);
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index 76bb27e..82b7413 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -39,6 +39,7 @@
 
     static_libs: [
         "FrameworksNetCommonTests",
+        "TestNetworkStackLib",
         "core-tests-support",
         "compatibility-device-util-axt",
         "cts-net-utils",
@@ -47,6 +48,7 @@
         "mockwebserver",
         "junit",
         "junit-params",
+        "libnanohttpd",
         "truth-prebuilt",
     ],
 
@@ -76,6 +78,7 @@
 android_test {
     name: "CtsNetTestCasesLatestSdk",
     defaults: ["CtsNetTestCasesDefaults"],
+    jni_uses_sdk_apis: true,
     min_sdk_version: "29",
     target_sdk_version: "29",
     test_suites: [
diff --git a/tests/cts/net/AndroidManifest.xml b/tests/cts/net/AndroidManifest.xml
index baf914f..a7e2bd7 100644
--- a/tests/cts/net/AndroidManifest.xml
+++ b/tests/cts/net/AndroidManifest.xml
@@ -26,6 +26,7 @@
     <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
     <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
     <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+    <uses-permission android:name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS" />
     <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
     <uses-permission android:name="android.permission.INTERNET" />
diff --git a/tests/cts/net/ipsec/Android.bp b/tests/cts/net/ipsec/Android.bp
index 1d9128b..124e93c 100644
--- a/tests/cts/net/ipsec/Android.bp
+++ b/tests/cts/net/ipsec/Android.bp
@@ -33,6 +33,7 @@
         "androidx.test.ext.junit",
         "compatibility-device-util-axt",
         "ctstestrunner-axt",
+        "net-tests-utils",
     ],
 
     platform_apis: true,
diff --git a/tests/cts/net/ipsec/AndroidTest.xml b/tests/cts/net/ipsec/AndroidTest.xml
index 09e5c93..cd5c118 100644
--- a/tests/cts/net/ipsec/AndroidTest.xml
+++ b/tests/cts/net/ipsec/AndroidTest.xml
@@ -27,4 +27,7 @@
         <option name="package" value="android.net.ipsec.cts" />
         <option name="hidden-api-checks" value="false" />
     </test>
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="mainline-module-package-name" value="com.google.android.ipsec" />
+    </object>
 </configuration>
diff --git a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionDigitalSignatureTest.java b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionDigitalSignatureTest.java
new file mode 100644
index 0000000..9be1dc7
--- /dev/null
+++ b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionDigitalSignatureTest.java
@@ -0,0 +1,211 @@
+/*
+ * 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 android.net.ipsec.ike.cts;
+
+import android.net.InetAddresses;
+import android.net.LinkAddress;
+import android.net.ipsec.ike.IkeDerAsn1DnIdentification;
+import android.net.ipsec.ike.IkeSession;
+import android.net.ipsec.ike.IkeSessionParams;
+import android.net.ipsec.ike.IkeTrafficSelector;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.internal.net.ipsec.ike.testutils.CertUtils;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetAddress;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.RSAPrivateKey;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import javax.security.auth.x500.X500Principal;
+
+/**
+ * Explicitly test setting up transport mode Child SA so that devices do not have
+ * FEATURE_IPSEC_TUNNELS will be test covered. Tunnel mode Child SA setup has been tested in
+ * IkeSessionPskTest and authentication method is orthogonal to Child mode.
+ */
+@RunWith(AndroidJUnit4.class)
+public class IkeSessionDigitalSignatureTest extends IkeSessionTestBase {
+    private static final int EXPECTED_AUTH_REQ_FRAG_COUNT = 3;
+
+    private static final String IKE_INIT_RESP =
+            "46B8ECA1E0D72A18BF3FA1C2CB1EE86F21202220000000000000015022000030"
+                    + "0000002C010100040300000C0100000C800E0100030000080300000503000008"
+                    + "0200000400000008040000022800008800020000328451C8A976CE69E407966A"
+                    + "50D7320C4197A15A07267CE1B16BAFF9BDBBDEC1FDCDAAF7175ADF9AA8DB55DB"
+                    + "2D70C012D01D914C4EDEF6E8B226868EA1D01B2ED0C4C5C86E6BFE566010EC0C"
+                    + "33BA1C93666430B88BDA0470D82CC4F4416F49E3E361E3017C9F27811A66718B"
+                    + "389E1800915D776D59AA528A7E1D1B7815D35144290000249FE8FABE7F43D917"
+                    + "CE370DE2FD9C22BBC082951AC26C1BA26DE795470F2C25BC2900001C00004004"
+                    + "AE388EC86D6D1A470D44142D01AB2E85A7AC14182900001C0000400544A235A4"
+                    + "171C884286B170F48FFC181DB428D87D290000080000402E290000100000402F"
+                    + "00020003000400050000000800004014";
+    private static final String IKE_AUTH_RESP_FRAG_1 =
+            "46B8ECA1E0D72A18BF3FA1C2CB1EE86F3520232000000001000004E0240004C4"
+                    + "00010002DF6750A2D1D5675006F9F6230BB886FFD20CFB973FD04963CFD7A528"
+                    + "560598C58CC44178B2FCBBBBB271387AC81A664B7E7F1055B912F8C686E287C9"
+                    + "D31684C66339151AB86DA3CF1DA664052FA97687634558A1E9E6B37E16A86BD1"
+                    + "68D76DA5E2E1E0B7E98EB662D80D542307015D2BF134EBBBE425D6954FE8C2C4"
+                    + "D31D16C16AA0521C3C481F873ECF25BB8B05AC6083775C1821CAAB1E35A3955D"
+                    + "85ACC599574142E1DD5B262D6E5365CBF6EBE92FFCC16BC29EC3239456F3B202"
+                    + "492551C0F6D752ADCCA56D506D50CC8809EF6BC56EAD005586F7168F76445FD3"
+                    + "1366CC62D32C0C19B28210B8F813F97CD6A447C3857EFD6EC483DDA8ACD9870E"
+                    + "5A21B9C66F0FA44496C0C3D05E8859A1A4CFC88155D0C411BABC13033DD41FA4"
+                    + "AF08CE7734A146687F374F95634D1F26843203CA1FFD05CA3EB150CEA02FBF14"
+                    + "712B7A1C9BC7616A086E7FCA059E7D64EFF98DB895B32F8F7002762AF7D12F23"
+                    + "31E9DD25174C4CE273E5392BBB48F50B7A3E0187181216265F6A4FC7B91BE0AB"
+                    + "C601A580149D4B07411AE99DDB1944B977E86ADC9746605C60A92B569EEFAFFC"
+                    + "3A888D187B75D8F13249689FC28EBCD62B5E03AF171F3A561F0DEA3B1A75F531"
+                    + "971157DCE1E7BC6E7789FF3E8156015BC9C521EFE48996B41471D33BF09864E4"
+                    + "2436E8D7EB6218CDE7716DA754A924B123A63E25585BF27F4AC043A0C4AECE38"
+                    + "BB59DD62F5C0EC657206A76CED1BD26262237DA1CA6815435992A825758DEBEC"
+                    + "DDF598A22B8242AC4E34E70704DBA7B7B73DC3E067C1C98764F8791F84C99156"
+                    + "947D1FFC875F36FCE24B89369C1B5BF1D4C999DCA28E72A528D0E0163C66C067"
+                    + "E71B5E0025C13DA93313942F9EDA230B3ADC254821A4CB1A5DC9D0C5F4DC4E8E"
+                    + "CE46B7B8C72D3C5923C9B30DF1EF7B4EDEDA8BD05C86CA0162AE1BF8F277878E"
+                    + "607401BAA8F06E3EA873FA4C137324C4E0699277CDF649FE7F0F01945EE25FA7"
+                    + "0E4A89737E58185B11B4CB52FD5B0497D3E3CD1CEE7B1FBB3E969DB6F4C324A1"
+                    + "32DC6A0EA21F41332435FD99140C286F8ABBBA926953ADBEED17D30AAD953909"
+                    + "1347EF6D87163D6B1FF32D8B11FFB2E69FAEE7FE913D3826FBA7F9D11E0E3C57"
+                    + "27625B37D213710B5DD8965DAEFD3F491E8C029E2BF361039949BADEC31D60AC"
+                    + "355F26EE41339C03CC9D9B01C3C7F288F0E9D6DFEE78231BDA9AC10FED135913"
+                    + "2836B1A17CE060742B7E5B738A7177CCD59F70337BA251409C377A0FA5333204"
+                    + "D8622BA8C06DE0BEF4F32B6D4D77BE9DE977445D8A2A08C5C38341CB7974FBFB"
+                    + "22C8F983A7D6CEF068DDB2281E6673453521C831C1826861005AE5F37649BC64"
+                    + "0A6360B23284861441A440F1C5AADE1AB53CA63DB17F4C314D493C4C44DE5F20"
+                    + "75E084D080F92791F30BDD88373D50AB5A07BC72B0E7FFFA593103964E55603E"
+                    + "F7FEB7CA0762A1A7B86B6CCAD88CD6CBC7C6935D21F5F06B2700588A2530E619"
+                    + "DA1648AC809F3DDF56ACE5951737568FFEC7E2AB1AA0AE01B03A7F5A29CE73C0"
+                    + "5D2801B17CAAD0121082E9952FAB16BA1C386336C62D4CF3A5019CF61609433E"
+                    + "1C083237D47C4CF575097F7BF9000EF6B6C497A44E6480154A35669AD276BF05"
+                    + "6CC730B4E5962B6AF96CC6D236AE85CEFDA6877173F72D2F614F6696D1F9DF07"
+                    + "E107758B0978F69BC9DBE0CCBF252C40A3FDF7CE9104D3344F7B73593CCD73E0";
+    private static final String IKE_AUTH_RESP_FRAG_2 =
+            "46B8ECA1E0D72A18BF3FA1C2CB1EE86F3520232000000001000000F0000000D4"
+                    + "00020002155211EA41B37BC5F20568A6AE57038EEE208F94F9B444004F1EF391"
+                    + "2CABFCF857B9CD95FAAA9489ED10A3F5C93510820E22E23FC55ED8049E067D72"
+                    + "3645C00E1E08611916CE72D7F0A84123B63A8F3B9E78DBBE39967B7BB074AF4D"
+                    + "BF2178D991EDBDD01908A14A266D09236DB963B14AC33D894F0F83A580209EFD"
+                    + "61875BB56273AA336C22D6A4D890B93E0D42435667830CC32E4F608500E18569"
+                    + "3E6C1D88C0B5AE427333C86468E3474DAA4D1506AAB2A4021309A33DD759D0D0"
+                    + "A8C98BF7FBEA8109361A9F194D0FD756";
+    private static final String DELETE_IKE_RESP =
+            "46B8ECA1E0D72A18BF3FA1C2CB1EE86F2E202520000000020000004C00000030"
+                    + "342842D8DA37C8EFB92ED37C4FBB23CBDC90445137D6A0AF489F9F03641DBA9D"
+                    + "02F6F59FD8A7A78C7261CEB8";
+
+    // Using IPv4 for transport mode Child SA. IPv6 is currently infeasible because the IKE server
+    // that generates the test vectors is running in an IPv4 only network.
+    private static final IkeTrafficSelector TRANSPORT_MODE_INBOUND_TS =
+            new IkeTrafficSelector(
+                    MIN_PORT,
+                    MAX_PORT,
+                    InetAddresses.parseNumericAddress("172.58.35.103"),
+                    InetAddresses.parseNumericAddress("172.58.35.103"));
+
+    // TODO(b/157510502): Add test for IKE Session setup with transport mode Child in IPv6 network
+
+    private static final String LOCAL_ID_ASN1_DN =
+            "CN=client.test.ike.android.net, O=Android, C=US";
+    private static final String REMOTE_ID_ASN1_DN =
+            "CN=server.test.ike.android.net, O=Android, C=US";
+
+    private static X509Certificate sServerCaCert;
+    private static X509Certificate sClientEndCert;
+    private static X509Certificate sClientIntermediateCaCertOne;
+    private static X509Certificate sClientIntermediateCaCertTwo;
+    private static RSAPrivateKey sClientPrivateKey;
+
+    @BeforeClass
+    public static void setUpCertsBeforeClass() throws Exception {
+        sServerCaCert = CertUtils.createCertFromPemFile("server-a-self-signed-ca.pem");
+        sClientEndCert = CertUtils.createCertFromPemFile("client-a-end-cert.pem");
+        sClientIntermediateCaCertOne =
+                CertUtils.createCertFromPemFile("client-a-intermediate-ca-one.pem");
+        sClientIntermediateCaCertTwo =
+                CertUtils.createCertFromPemFile("client-a-intermediate-ca-two.pem");
+        sClientPrivateKey = CertUtils.createRsaPrivateKeyFromKeyFile("client-a-private-key.key");
+    }
+
+    private IkeSession openIkeSessionWithRemoteAddress(InetAddress remoteAddress) {
+        IkeSessionParams ikeParams =
+                new IkeSessionParams.Builder(sContext)
+                        .setNetwork(mTunNetwork)
+                        .setServerHostname(remoteAddress.getHostAddress())
+                        .addSaProposal(SaProposalTest.buildIkeSaProposalWithNormalModeCipher())
+                        .addSaProposal(SaProposalTest.buildIkeSaProposalWithCombinedModeCipher())
+                        .setLocalIdentification(
+                                new IkeDerAsn1DnIdentification(new X500Principal(LOCAL_ID_ASN1_DN)))
+                        .setRemoteIdentification(
+                                new IkeDerAsn1DnIdentification(
+                                        new X500Principal(REMOTE_ID_ASN1_DN)))
+                        .setAuthDigitalSignature(
+                                sServerCaCert,
+                                sClientEndCert,
+                                Arrays.asList(
+                                        sClientIntermediateCaCertOne, sClientIntermediateCaCertTwo),
+                                sClientPrivateKey)
+                        .build();
+
+        return new IkeSession(
+                sContext,
+                ikeParams,
+                buildTransportModeChildParamsWithTs(
+                        TRANSPORT_MODE_INBOUND_TS, TRANSPORT_MODE_OUTBOUND_TS),
+                mUserCbExecutor,
+                mIkeSessionCallback,
+                mFirstChildSessionCallback);
+    }
+
+    @Test
+    public void testIkeSessionSetupAndChildSessionSetupWithTransportMode() throws Exception {
+        // Open IKE Session
+        IkeSession ikeSession = openIkeSessionWithRemoteAddress(mRemoteAddress);
+        performSetupIkeAndFirstChildBlocking(
+                IKE_INIT_RESP,
+                EXPECTED_AUTH_REQ_FRAG_COUNT /* expectedReqPktCnt */,
+                true /* expectedAuthUseEncap */,
+                IKE_AUTH_RESP_FRAG_1,
+                IKE_AUTH_RESP_FRAG_2);
+
+        // IKE INIT and IKE AUTH takes two exchanges. Message ID starts from 2
+        int expectedMsgId = 2;
+
+        verifyIkeSessionSetupBlocking();
+        verifyChildSessionSetupBlocking(
+                mFirstChildSessionCallback,
+                Arrays.asList(TRANSPORT_MODE_INBOUND_TS),
+                Arrays.asList(TRANSPORT_MODE_OUTBOUND_TS),
+                new ArrayList<LinkAddress>());
+        IpSecTransformCallRecord firstTransformRecordA =
+                mFirstChildSessionCallback.awaitNextCreatedIpSecTransform();
+        IpSecTransformCallRecord firstTransformRecordB =
+                mFirstChildSessionCallback.awaitNextCreatedIpSecTransform();
+        verifyCreateIpSecTransformPair(firstTransformRecordA, firstTransformRecordB);
+
+        // Close IKE Session
+        ikeSession.close();
+        performCloseIkeBlocking(expectedMsgId++, DELETE_IKE_RESP);
+        verifyCloseIkeAndChildBlocking(firstTransformRecordA, firstTransformRecordB);
+    }
+}
diff --git a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionMschapV2Test.java b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionMschapV2Test.java
new file mode 100644
index 0000000..cb77127
--- /dev/null
+++ b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionMschapV2Test.java
@@ -0,0 +1,220 @@
+/*
+ * 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 android.net.ipsec.ike.cts;
+
+import android.net.InetAddresses;
+import android.net.LinkAddress;
+import android.net.eap.EapSessionConfig;
+import android.net.ipsec.ike.IkeFqdnIdentification;
+import android.net.ipsec.ike.IkeSession;
+import android.net.ipsec.ike.IkeSessionParams;
+import android.net.ipsec.ike.IkeTrafficSelector;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.internal.net.ipsec.ike.testutils.CertUtils;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetAddress;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Explicitly test setting up transport mode Child SA so that devices do not have
+ * FEATURE_IPSEC_TUNNELS will be test covered. Tunnel mode Child SA setup has been tested in
+ * IkeSessionPskTest and authentication method is orthogonal to Child mode.
+ */
+@RunWith(AndroidJUnit4.class)
+public class IkeSessionMschapV2Test extends IkeSessionTestBase {
+    private static final String IKE_INIT_RESP =
+            "46B8ECA1E0D72A1873F643FF94D249A921202220000000000000015022000030"
+                    + "0000002C010100040300000C0100000C800E0080030000080300000203000008"
+                    + "0200000200000008040000022800008800020000CC6E71E67E32CED6BCE33FBD"
+                    + "A74113867E3FA3AE21C7C9AB44A7F8835DF602BFD6F6528B67FEE39821232380"
+                    + "C99E8FFC0A5D767F8F38906DA41946C2299DF18C15FA69BAC08D3EDB32E8C8CA"
+                    + "28431831561C04CB0CDE393F817151CD8DAF7A311838411F1C39BFDB5EBCF6A6"
+                    + "1DF66DEB067362649D64607D599B56C4227819D0290000241197004CF31AD00F"
+                    + "5E0C92E198488D8A2B6F6A25C82762AA49F565BCE9D857D72900001C00004004"
+                    + "A0D98FEABBFB92A6C0976EE83D2AACFCCF969A6B2900001C0000400575EBF73F"
+                    + "8EE5CC73917DE9D3F91FCD4A16A0444D290000080000402E290000100000402F"
+                    + "00020003000400050000000800004014";
+    private static final String IKE_AUTH_RESP_1_FRAG_1 =
+            "46B8ECA1E0D72A1873F643FF94D249A93520232000000001000004E0240004C4"
+                    + "00010002C4159CB756773B3F1911F4595107BC505D7A28C72F05182966076679"
+                    + "CA68ED92E4BC5CD441C9CB315F2F449A8A521CAFED3C5F285E295FC3791D3415"
+                    + "E3BACF66A08410DF4E35F7D88FE40DA28851C91C77A6549E186AC1B7846DF3FA"
+                    + "0A347A5ABBCAEE19E70F0EE5966DC6242A115F29523709302EDAD2E36C8F0395"
+                    + "CF5C42EC2D2898ECDD8A6AEDD686A70B589A981558667647F32F41E0D8913E94"
+                    + "A6693F53E59EA8938037F562CF1DC5E6E2CDC630B5FFB08949E3172249422F7D"
+                    + "EA069F9BAD5F96E48BADC7164A9269669AD0DF295A80C54D1D23CEA3F28AC485"
+                    + "86D2A9850DA23823037AB7D1577B7B2364C92C36B84238357129EB4A64D33310"
+                    + "B95DCD50CD53E78C32EFE7DC1627D9432E9BFDEE130045DE967B19F92A9D1270"
+                    + "F1E2C6BFBAA56802F3E63510578EF1ECB6872852F286EEC790AA1FE0CAF391CB"
+                    + "E276554922713BA4770CFE71E23F043DC620E22CC02A74F60725D18331B7F2C9"
+                    + "276EB6FBB7CBDAA040046D7ECBE1A5D7064E04E542807C5101B941D1C81B9D5E"
+                    + "90347B22BD4E638E2EDC98E369B51AA29BDB2CF8AA610D4B893EB83A4650717C"
+                    + "38B4D145EE939C18DCEDF6C79933CEB3D7C116B1F188DF9DDD560951B54E4A7D"
+                    + "80C999A32AB02BF39D7B498DAD36F1A5CBE2F64557D6401AE9DD6E0CEADA3F90"
+                    + "540FE9114BB6B8719C9064796354F4A180A6600CAD092F8302564E409B71ACB7"
+                    + "590F19B3AC88E7A606C718D0B97F7E4B4830F11D851C59F2255846DA22E2C805"
+                    + "0CA2AF2ACF3B6C769D11B75B5AC9AB82ED3D90014994B1BF6FED58FBEF2D72EF"
+                    + "8BDFE51F9A101393A7CA1ACF78FAEBF3E3CC25E09407D1E14AF351A159A13EE3"
+                    + "9B919BA8B49942792E7527C2FB6D418C4DF427669A4BF5A1AFBBB973BAF17918"
+                    + "9C9D520CAC2283B89A539ECE785EBE48FBB77D880A17D55C84A51F46068A4B87"
+                    + "FF48FEEE50E1E034CC8AFF5DA92105F55EC4823E67BDFE942CA8BE0DAECBBD52"
+                    + "E8AAF306049DC6C4CF87D987B0AC54FCE92E6AE8507965AAAC6AB8BD3405712F"
+                    + "EE170B70BC64BDCBD86D80C7AAAF341131F9A1210D7430B17218413AE1363183"
+                    + "5C98FA2428B1E9E987ADC9070E232310A28F4C3163E18366FFB112BADD7C5E0F"
+                    + "D13093A7C1428F87856BA0A7E46955589ACA267CE7A04320C4BCDBB60C672404"
+                    + "778F8D511AAB09349DAB482445D7F606F28E7FBBB18FC0F4EC0AF04F44C282F9"
+                    + "39C6E3B955C84DADEA350667236583069B74F492D600127636FA31F63E560851"
+                    + "2FC28B8EA5B4D01D110990B6EA46B9C2E7C7C856C240EF7A8147BA2C4344B85A"
+                    + "453C862024B5B6814D13CDEAEF7683D539BB50CAFFC0416F269F2F9EDEC5FA30"
+                    + "022FD7B4B186CD2020E7ED8D81ED90822EDD8B76F840DD68F09694CFF9B4F33E"
+                    + "11DF4E601A4212881A6D4E9259001705C41E9E23D18A7F3D4A3463649A38211A"
+                    + "5A90D0F17739A677C74E23F31C01D60B5A0F1E6A4D44FED9D25BF1E63418E1FC"
+                    + "0B19F6F4B71DE53C62B14B82279538A82DD4BE19AB6E00AFC20F124AAB7DF21A"
+                    + "42259BE4F40EC69B16917256F23E2C37376311D62E0A3A0EF8C2AD0C090221D5"
+                    + "C5ECA08F08178A4D31FFDB150C609827D18AD83C7B0A43AEE0406BD3FB494B53"
+                    + "A279FDD6447E234C926AD8CE47FFF779BB45B1FC8457C6E7D257D1359959D977"
+                    + "CEF6906A3367DC4D454993EFDC6F1EA94E17EB3DCB00A289346B4CFD7F19B16E";
+    private static final String IKE_AUTH_RESP_1_FRAG_2 =
+            "46B8ECA1E0D72A1873F643FF94D249A935202320000000010000008000000064"
+                    + "00020002C61F66025E821A5E69A4DE1F591A2C32C983C3154A5003660137D685"
+                    + "A5262B9FDF5EDC699DE4D8BD38F549E3CBD12024B45B4C86561C36C3EED839DA"
+                    + "9860C6AA0B764C662D08F1B6A98F68CF6E3038F737C0B415AD8A8B7D702BD92A";
+    private static final String IKE_AUTH_RESP_2 =
+            "46B8ECA1E0D72A1873F643FF94D249A92E202320000000020000008C30000070"
+                    + "62B90C2229FD23025BC2FD7FE6341E9EE04B17264CD619BCE18975A5F88BE438"
+                    + "D4AD4A5310057255AF568C293A29B10107E3EE3675C10AA2B26404D90C0528CC"
+                    + "F7605A86C96A1F2635CCC6CFC90EE65E5C2A2262EB33FE520EB708423A83CB63"
+                    + "274ECCBB102AF5DF35742657";
+    private static final String IKE_AUTH_RESP_3 =
+            "46B8ECA1E0D72A1873F643FF94D249A92E202320000000030000004C30000030"
+                    + "AB52C3C80123D3432C05AF457CE93C352395F73E861CD49561BA528CFE68D17D"
+                    + "78BBF6FC41E81C2B9EA051A2";
+    private static final String IKE_AUTH_RESP_4 =
+            "46B8ECA1E0D72A1873F643FF94D249A92E20232000000004000000CC270000B0"
+                    + "8D3342A7AB2666AC754F4B55C5C6B1A61255E62FBCA53D5CDEEDE60DADB7915C"
+                    + "7F962076A58BF7D39A05ED1B60FF349B6DE311AF7CEBC72B4BB9723A728A5D3E"
+                    + "9E508B2D7A11843D279B56ADA07E608D61F5CA7638F10372A440AD1DCE44E190"
+                    + "7B7B7A68B126EBBB86638D667D5B528D233BA8D32D7E0FAC4E1448E87396EEE6"
+                    + "0985B79841E1229D7962AACFD8F872722EC8D5B19D4C82D6C4ADCB276127A1A7"
+                    + "3FC84CDF85B2299BC96B64AC";
+    private static final String DELETE_IKE_RESP =
+            "46B8ECA1E0D72A1873F643FF94D249A92E202520000000050000004C00000030"
+                    + "622CE06C8CB132AA00567E9BC83F58B32BD7DB5130C76E385B306434DA227361"
+                    + "D50CC19D408A8D4F36F9697F";
+
+    // This value is align with the test vectors hex that are generated in an IPv4 environment
+    private static final IkeTrafficSelector TRANSPORT_MODE_INBOUND_TS =
+            new IkeTrafficSelector(
+                    MIN_PORT,
+                    MAX_PORT,
+                    InetAddresses.parseNumericAddress("172.58.35.67"),
+                    InetAddresses.parseNumericAddress("172.58.35.67"));
+
+    private static final EapSessionConfig EAP_CONFIG =
+            new EapSessionConfig.Builder()
+                    .setEapIdentity(EAP_IDENTITY)
+                    .setEapMsChapV2Config(EAP_MSCHAPV2_USERNAME, EAP_MSCHAPV2_PASSWORD)
+                    .build();
+
+    private static X509Certificate sServerCaCert;
+
+    @BeforeClass
+    public static void setUpCertBeforeClass() throws Exception {
+        sServerCaCert = CertUtils.createCertFromPemFile("server-a-self-signed-ca.pem");
+    }
+
+    private IkeSession openIkeSessionWithRemoteAddress(InetAddress remoteAddress) {
+        IkeSessionParams ikeParams =
+                new IkeSessionParams.Builder(sContext)
+                        .setNetwork(mTunNetwork)
+                        .setServerHostname(remoteAddress.getHostAddress())
+                        .addSaProposal(SaProposalTest.buildIkeSaProposalWithNormalModeCipher())
+                        .addSaProposal(SaProposalTest.buildIkeSaProposalWithCombinedModeCipher())
+                        .setLocalIdentification(new IkeFqdnIdentification(LOCAL_HOSTNAME))
+                        .setRemoteIdentification(new IkeFqdnIdentification(REMOTE_HOSTNAME))
+                        .setAuthEap(sServerCaCert, EAP_CONFIG)
+                        .build();
+        return new IkeSession(
+                sContext,
+                ikeParams,
+                buildTransportModeChildParamsWithTs(
+                        TRANSPORT_MODE_INBOUND_TS, TRANSPORT_MODE_OUTBOUND_TS),
+                mUserCbExecutor,
+                mIkeSessionCallback,
+                mFirstChildSessionCallback);
+    }
+
+    @Test
+    public void testIkeSessionSetupAndChildSessionSetupWithTransportMode() throws Exception {
+        // Open IKE Session
+        IkeSession ikeSession = openIkeSessionWithRemoteAddress(mRemoteAddress);
+        int expectedMsgId = 0;
+        mTunUtils.awaitReqAndInjectResp(
+                IKE_DETERMINISTIC_INITIATOR_SPI,
+                expectedMsgId++,
+                false /* expectedUseEncap */,
+                IKE_INIT_RESP);
+
+        mTunUtils.awaitReqAndInjectResp(
+                IKE_DETERMINISTIC_INITIATOR_SPI,
+                expectedMsgId++,
+                true /* expectedUseEncap */,
+                IKE_AUTH_RESP_1_FRAG_1,
+                IKE_AUTH_RESP_1_FRAG_2);
+
+        mTunUtils.awaitReqAndInjectResp(
+                IKE_DETERMINISTIC_INITIATOR_SPI,
+                expectedMsgId++,
+                true /* expectedUseEncap */,
+                IKE_AUTH_RESP_2);
+        mTunUtils.awaitReqAndInjectResp(
+                IKE_DETERMINISTIC_INITIATOR_SPI,
+                expectedMsgId++,
+                true /* expectedUseEncap */,
+                IKE_AUTH_RESP_3);
+        mTunUtils.awaitReqAndInjectResp(
+                IKE_DETERMINISTIC_INITIATOR_SPI,
+                expectedMsgId++,
+                true /* expectedUseEncap */,
+                IKE_AUTH_RESP_4);
+
+        verifyIkeSessionSetupBlocking();
+        verifyChildSessionSetupBlocking(
+                mFirstChildSessionCallback,
+                Arrays.asList(TRANSPORT_MODE_INBOUND_TS),
+                Arrays.asList(TRANSPORT_MODE_OUTBOUND_TS),
+                new ArrayList<LinkAddress>());
+        IpSecTransformCallRecord firstTransformRecordA =
+                mFirstChildSessionCallback.awaitNextCreatedIpSecTransform();
+        IpSecTransformCallRecord firstTransformRecordB =
+                mFirstChildSessionCallback.awaitNextCreatedIpSecTransform();
+        verifyCreateIpSecTransformPair(firstTransformRecordA, firstTransformRecordB);
+
+        // Close IKE Session
+        ikeSession.close();
+        performCloseIkeBlocking(expectedMsgId++, DELETE_IKE_RESP);
+        verifyCloseIkeAndChildBlocking(firstTransformRecordA, firstTransformRecordB);
+    }
+}
diff --git a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionParamsTest.java b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionParamsTest.java
index 6fc7cb3..c767b78 100644
--- a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionParamsTest.java
+++ b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionParamsTest.java
@@ -46,6 +46,7 @@
 
 import com.android.internal.net.ipsec.ike.testutils.CertUtils;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -63,7 +64,7 @@
 import java.util.concurrent.TimeUnit;
 
 @RunWith(AndroidJUnit4.class)
-public final class IkeSessionParamsTest extends IkeSessionParamsTestBase {
+public final class IkeSessionParamsTest extends IkeSessionTestBase {
     private static final int HARD_LIFETIME_SECONDS = (int) TimeUnit.HOURS.toSeconds(20L);
     private static final int SOFT_LIFETIME_SECONDS = (int) TimeUnit.HOURS.toSeconds(10L);
     private static final int DPD_DELAY_SECONDS = (int) TimeUnit.MINUTES.toSeconds(10L);
@@ -105,6 +106,9 @@
 
     @Before
     public void setUp() throws Exception {
+        // This address is never used except for setting up the test network
+        setUpTestNetwork(IPV4_ADDRESS_LOCAL);
+
         mServerCaCert = CertUtils.createCertFromPemFile("server-a-self-signed-ca.pem");
         mClientEndCert = CertUtils.createCertFromPemFile("client-a-end-cert.pem");
         mClientIntermediateCaCertOne =
@@ -114,6 +118,11 @@
         mClientPrivateKey = CertUtils.createRsaPrivateKeyFromKeyFile("client-a-private-key.key");
     }
 
+    @After
+    public void tearDown() throws Exception {
+        tearDownTestNetwork();
+    }
+
     private static EapSessionConfig.Builder createEapOnlySafeMethodsBuilder() {
         return new EapSessionConfig.Builder()
                 .setEapIdentity(EAP_IDENTITY)
@@ -131,7 +140,7 @@
      */
     private IkeSessionParams.Builder createIkeParamsBuilderMinimum() {
         return new IkeSessionParams.Builder(sContext)
-                .setNetwork(sTunNetwork)
+                .setNetwork(mTunNetwork)
                 .setServerHostname(IPV4_ADDRESS_REMOTE.getHostAddress())
                 .addSaProposal(SA_PROPOSAL)
                 .setLocalIdentification(LOCAL_ID)
@@ -145,7 +154,7 @@
      * @see #createIkeParamsBuilderMinimum
      */
     private void verifyIkeParamsMinimum(IkeSessionParams sessionParams) {
-        assertEquals(sTunNetwork, sessionParams.getNetwork());
+        assertEquals(mTunNetwork, sessionParams.getNetwork());
         assertEquals(IPV4_ADDRESS_REMOTE.getHostAddress(), sessionParams.getServerHostname());
         assertEquals(Arrays.asList(SA_PROPOSAL), sessionParams.getSaProposals());
         assertEquals(LOCAL_ID, sessionParams.getLocalIdentification());
@@ -268,7 +277,7 @@
      */
     private IkeSessionParams.Builder createIkeParamsBuilderMinimumWithoutAuth() {
         return new IkeSessionParams.Builder(sContext)
-                .setNetwork(sTunNetwork)
+                .setNetwork(mTunNetwork)
                 .setServerHostname(IPV4_ADDRESS_REMOTE.getHostAddress())
                 .addSaProposal(SA_PROPOSAL)
                 .setLocalIdentification(LOCAL_ID)
@@ -282,7 +291,7 @@
      * @see #createIkeParamsBuilderMinimumWithoutAuth
      */
     private void verifyIkeParamsMinimumWithoutAuth(IkeSessionParams sessionParams) {
-        assertEquals(sTunNetwork, sessionParams.getNetwork());
+        assertEquals(mTunNetwork, sessionParams.getNetwork());
         assertEquals(IPV4_ADDRESS_REMOTE.getHostAddress(), sessionParams.getServerHostname());
         assertEquals(Arrays.asList(SA_PROPOSAL), sessionParams.getSaProposals());
         assertEquals(LOCAL_ID, sessionParams.getLocalIdentification());
diff --git a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionParamsTestBase.java b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionParamsTestBase.java
deleted file mode 100644
index c3e3ba3..0000000
--- a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionParamsTestBase.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.net.ipsec.ike.cts;
-
-import android.content.Context;
-import android.net.ConnectivityManager;
-import android.net.LinkAddress;
-import android.net.Network;
-import android.net.TestNetworkInterface;
-import android.net.TestNetworkManager;
-import android.net.ipsec.ike.cts.TestNetworkUtils.TestNetworkCallback;
-import android.os.Binder;
-import android.os.IBinder;
-import android.os.ParcelFileDescriptor;
-import android.platform.test.annotations.AppModeFull;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-@AppModeFull(reason = "MANAGE_TEST_NETWORKS permission can't be granted to instant apps")
-abstract class IkeSessionParamsTestBase extends IkeTestBase {
-    // Static state to reduce setup/teardown
-    static ConnectivityManager sCM;
-    static TestNetworkManager sTNM;
-    static ParcelFileDescriptor sTunFd;
-    static TestNetworkCallback sTunNetworkCallback;
-    static Network sTunNetwork;
-
-    static Context sContext = InstrumentationRegistry.getContext();
-    static IBinder sBinder = new Binder();
-
-    // This method is guaranteed to run in subclasses and will run before subclasses' @BeforeClass
-    // methods.
-    @BeforeClass
-    public static void setUpTestNetworkBeforeClass() throws Exception {
-        InstrumentationRegistry.getInstrumentation()
-                .getUiAutomation()
-                .adoptShellPermissionIdentity();
-        sCM = (ConnectivityManager) sContext.getSystemService(Context.CONNECTIVITY_SERVICE);
-        sTNM = (TestNetworkManager) sContext.getSystemService(Context.TEST_NETWORK_SERVICE);
-
-        TestNetworkInterface testIface =
-                sTNM.createTunInterface(
-                        new LinkAddress[] {new LinkAddress(IPV4_ADDRESS_LOCAL, IP4_PREFIX_LEN)});
-
-        sTunFd = testIface.getFileDescriptor();
-        sTunNetworkCallback =
-                TestNetworkUtils.setupAndGetTestNetwork(
-                        sCM, sTNM, testIface.getInterfaceName(), sBinder);
-        sTunNetwork = sTunNetworkCallback.getNetworkBlocking();
-    }
-
-    // This method is guaranteed to run in subclasses and will run after subclasses' @AfterClass
-    // methods.
-    @AfterClass
-    public static void tearDownTestNetworkAfterClass() throws Exception {
-        sCM.unregisterNetworkCallback(sTunNetworkCallback);
-
-        sTNM.teardownTestNetwork(sTunNetwork);
-        sTunFd.close();
-
-        InstrumentationRegistry.getInstrumentation()
-                .getUiAutomation()
-                .dropShellPermissionIdentity();
-    }
-}
diff --git a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionPskTest.java b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionPskTest.java
new file mode 100644
index 0000000..0509fc0
--- /dev/null
+++ b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionPskTest.java
@@ -0,0 +1,361 @@
+/*
+ * 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 android.net.ipsec.ike.cts;
+
+import static android.app.AppOpsManager.OP_MANAGE_IPSEC_TUNNELS;
+import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_AUTHENTICATION_FAILED;
+import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_INTERNAL_ADDRESS_FAILURE;
+import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_NO_PROPOSAL_CHOSEN;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.net.LinkAddress;
+import android.net.ipsec.ike.IkeFqdnIdentification;
+import android.net.ipsec.ike.IkeSession;
+import android.net.ipsec.ike.IkeSessionParams;
+import android.net.ipsec.ike.exceptions.IkeProtocolException;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "MANAGE_IPSEC_TUNNELS permission can't be granted to instant apps")
+public class IkeSessionPskTest extends IkeSessionTestBase {
+    // Test vectors for success workflow
+    private static final String SUCCESS_IKE_INIT_RESP =
+            "46B8ECA1E0D72A18B45427679F9245D421202220000000000000015022000030"
+                    + "0000002C010100040300000C0100000C800E0080030000080300000203000008"
+                    + "0200000200000008040000022800008800020000A7AA3435D088EC1A2B7C2A47"
+                    + "1FA1B85F1066C9B2006E7C353FB5B5FDBC2A88347ED2C6F5B7A265D03AE34039"
+                    + "6AAC0145CFCC93F8BDB219DDFF22A603B8856A5DC59B6FAB7F17C5660CF38670"
+                    + "8794FC72F273ADEB7A4F316519794AED6F8AB61F95DFB360FAF18C6C8CABE471"
+                    + "6E18FE215348C2E582171A57FC41146B16C4AFE429000024A634B61C0E5C90C6"
+                    + "8D8818B0955B125A9B1DF47BBD18775710792E651083105C2900001C00004004"
+                    + "406FA3C5685A16B9B72C7F2EEE9993462C619ABE2900001C00004005AF905A87"
+                    + "0A32222AA284A7070585601208A282F0290000080000402E290000100000402F"
+                    + "00020003000400050000000800004014";
+    private static final String SUCCESS_IKE_AUTH_RESP =
+            "46B8ECA1E0D72A18B45427679F9245D42E20232000000001000000EC240000D0"
+                    + "0D06D37198F3F0962DE8170D66F1A9008267F98CDD956D984BDCED2FC7FAF84A"
+                    + "A6664EF25049B46B93C9ED420488E0C172AA6635BF4011C49792EF2B88FE7190"
+                    + "E8859FEEF51724FD20C46E7B9A9C3DC4708EF7005707A18AB747C903ABCEAC5C"
+                    + "6ECF5A5FC13633DCE3844A920ED10EF202F115DBFBB5D6D2D7AB1F34EB08DE7C"
+                    + "A54DCE0A3A582753345CA2D05A0EFDB9DC61E81B2483B7D13EEE0A815D37252C"
+                    + "23D2F29E9C30658227D2BB0C9E1A481EAA80BC6BE9006BEDC13E925A755A0290"
+                    + "AEC4164D29997F52ED7DCC2E";
+    private static final String SUCCESS_CREATE_CHILD_RESP =
+            "46B8ECA1E0D72A18B45427679F9245D42E20242000000002000000CC210000B0"
+                    + "484565D4AF6546274674A8DE339E9C9584EE2326AB9260F41C4D0B6C5B02D1D"
+                    + "2E8394E3CDE3094895F2ACCABCDCA8E82960E5196E9622BD13745FC8D6A2BED"
+                    + "E561FF5D9975421BC463C959A3CBA3478256B6D278159D99B512DDF56AC1658"
+                    + "63C65A986F395FE8B1476124B91F83FD7865304EB95B22CA4DD9601DA7A2533"
+                    + "ABF4B36EB1B8CD09522F6A600032316C74E562E6756D9D49D945854E2ABDC4C"
+                    + "3AF36305353D60D40B58BE44ABF82";
+    private static final String SUCCESS_DELETE_CHILD_RESP =
+            "46B8ECA1E0D72A18B45427679F9245D42E202520000000030000004C2A000030"
+                    + "0C5CEB882DBCA65CE32F4C53909335F1365C91C555316C5E9D9FB553F7AA916"
+                    + "EF3A1D93460B7FABAF0B4B854";
+    private static final String SUCCESS_DELETE_IKE_RESP =
+            "46B8ECA1E0D72A18B45427679F9245D42E202520000000040000004C00000030"
+                    + "9352D71100777B00ABCC6BD7DBEA697827FFAAA48DF9A54D1D68161939F5DC8"
+                    + "6743A7CEB2BE34AC00095A5B8";
+
+    private IkeSession openIkeSessionWithRemoteAddress(InetAddress remoteAddress) {
+        IkeSessionParams ikeParams =
+                new IkeSessionParams.Builder(sContext)
+                        .setNetwork(mTunNetwork)
+                        .setServerHostname(remoteAddress.getHostAddress())
+                        .addSaProposal(SaProposalTest.buildIkeSaProposalWithNormalModeCipher())
+                        .addSaProposal(SaProposalTest.buildIkeSaProposalWithCombinedModeCipher())
+                        .setLocalIdentification(new IkeFqdnIdentification(LOCAL_HOSTNAME))
+                        .setRemoteIdentification(new IkeFqdnIdentification(REMOTE_HOSTNAME))
+                        .setAuthPsk(IKE_PSK)
+                        .build();
+        return new IkeSession(
+                sContext,
+                ikeParams,
+                buildTunnelModeChildSessionParams(),
+                mUserCbExecutor,
+                mIkeSessionCallback,
+                mFirstChildSessionCallback);
+    }
+
+    @BeforeClass
+    public static void setUpTunnelPermissionBeforeClass() throws Exception {
+        // Under normal circumstances, the MANAGE_IPSEC_TUNNELS appop would be auto-granted, and
+        // a standard permission is insufficient. So we shell out the appop, to give us the
+        // right appop permissions.
+        setAppOp(OP_MANAGE_IPSEC_TUNNELS, true);
+    }
+
+    // This method is guaranteed to run in subclasses and will run after subclasses' @AfterClass
+    // methods.
+    @AfterClass
+    public static void tearDownTunnelPermissionAfterClass() throws Exception {
+        setAppOp(OP_MANAGE_IPSEC_TUNNELS, false);
+    }
+
+    @Test
+    public void testIkeSessionSetupAndChildSessionSetupWithTunnelMode() throws Exception {
+        if (!hasTunnelsFeature()) return;
+
+        // Open IKE Session
+        IkeSession ikeSession = openIkeSessionWithRemoteAddress(mRemoteAddress);
+        performSetupIkeAndFirstChildBlocking(SUCCESS_IKE_INIT_RESP, SUCCESS_IKE_AUTH_RESP);
+
+        // IKE INIT and IKE AUTH takes two exchanges. Message ID starts from 2
+        int expectedMsgId = 2;
+
+        verifyIkeSessionSetupBlocking();
+        verifyChildSessionSetupBlocking(
+                mFirstChildSessionCallback,
+                Arrays.asList(TUNNEL_MODE_INBOUND_TS),
+                Arrays.asList(TUNNEL_MODE_OUTBOUND_TS),
+                Arrays.asList(EXPECTED_INTERNAL_LINK_ADDR));
+
+        IpSecTransformCallRecord firstTransformRecordA =
+                mFirstChildSessionCallback.awaitNextCreatedIpSecTransform();
+        IpSecTransformCallRecord firstTransformRecordB =
+                mFirstChildSessionCallback.awaitNextCreatedIpSecTransform();
+        verifyCreateIpSecTransformPair(firstTransformRecordA, firstTransformRecordB);
+
+        // Open additional Child Session
+        TestChildSessionCallback additionalChildCb = new TestChildSessionCallback();
+        ikeSession.openChildSession(buildTunnelModeChildSessionParams(), additionalChildCb);
+        mTunUtils.awaitReqAndInjectResp(
+                IKE_DETERMINISTIC_INITIATOR_SPI,
+                expectedMsgId++,
+                true /* expectedUseEncap */,
+                SUCCESS_CREATE_CHILD_RESP);
+
+        // Verify opening additional Child Session
+        verifyChildSessionSetupBlocking(
+                additionalChildCb,
+                Arrays.asList(TUNNEL_MODE_INBOUND_TS),
+                Arrays.asList(TUNNEL_MODE_OUTBOUND_TS),
+                new ArrayList<LinkAddress>());
+        IpSecTransformCallRecord additionalTransformRecordA =
+                additionalChildCb.awaitNextCreatedIpSecTransform();
+        IpSecTransformCallRecord additionalTransformRecordB =
+                additionalChildCb.awaitNextCreatedIpSecTransform();
+        verifyCreateIpSecTransformPair(additionalTransformRecordA, additionalTransformRecordB);
+
+        // Close additional Child Session
+        ikeSession.closeChildSession(additionalChildCb);
+        mTunUtils.awaitReqAndInjectResp(
+                IKE_DETERMINISTIC_INITIATOR_SPI,
+                expectedMsgId++,
+                true /* expectedUseEncap */,
+                SUCCESS_DELETE_CHILD_RESP);
+
+        verifyDeleteIpSecTransformPair(
+                additionalChildCb, additionalTransformRecordA, additionalTransformRecordB);
+        additionalChildCb.awaitOnClosed();
+
+        // Close IKE Session
+        ikeSession.close();
+        performCloseIkeBlocking(expectedMsgId++, SUCCESS_DELETE_IKE_RESP);
+        verifyCloseIkeAndChildBlocking(firstTransformRecordA, firstTransformRecordB);
+    }
+
+    @Test
+    public void testIkeSessionSetupAndChildSessionSetupWithTunnelModeV6() throws Exception {
+        if (!hasTunnelsFeature()) return;
+
+        final String ikeInitResp =
+                "46B8ECA1E0D72A186F7B6C2CEB77EB9021202220000000000000011822000030"
+                        + "0000002C010100040300000C0100000C800E0100030000080300000C03000008"
+                        + "0200000500000008040000022800008800020000DABAA04B38B491E2403F2125"
+                        + "96ECF1C8EF7B1DC19A422FDD46E1756C826BB3A16404361B775D9950577B5CDF"
+                        + "6AAA1642BD1427BDA8BC55354A97C1025E19C1E2EE2DF8A0C9406E545D829F52"
+                        + "75695008E3B742984B8DD1770F3514213B0DF3EE8B199416DF200D248115C057"
+                        + "1C193E4F96802E5EF48DD99CAC251882A8F7CCC329000024BC6F0F1D3653C2C7"
+                        + "679E02CDB6A3B32B2FEE9AF52F0326D4D9AE073D56CE8922290000080000402E"
+                        + "290000100000402F00020003000400050000000800004014";
+        final String ikeAuthResp =
+                "46B8ECA1E0D72A186F7B6C2CEB77EB902E202320000000010000015024000134"
+                        + "4D115AFDCDAD0310760BB664EB7D405A340869AD6EDF0AAEAD0663A9253DADCB"
+                        + "73EBE5CD29D4FA1CDEADE0B94391B5C4CF77BCC1596ACE3CE6A7891E44888FA5"
+                        + "46632C0EF4E6193C023C9DC59142C37D1C49D6EF5CD324EC6FC35C89E1721C78"
+                        + "91FDCDB723D8062709950F4AA9273D26A54C9C7E86862DBC15F7B6641D2B9BAD"
+                        + "E55069008201D12968D97B537B1518FE87B0FFA03C3EE6012C06721B1E2A3F68"
+                        + "92108BC4A4F7063F7F94562D8B60F291A1377A836CF12BCDA7E15C1A8F3C77BB"
+                        + "6DB7F2C833CCE4CDDED7506536621A3356CE2BC1874E7B1A1A9B447D7DF6AB09"
+                        + "638B8AD94A781B28BB91B514B611B24DF8E8A047A10AE27BBF15C754D3D2F792"
+                        + "D3E1CCADDAE934C98AE53A8FC3419C88AFF0355564F82A629C998012DA7BB704"
+                        + "5307270DF326377E3E1994476902035B";
+        final String deleteIkeResp =
+                "46B8ECA1E0D72A186F7B6C2CEB77EB902E202520000000020000005000000034"
+                        + "CF15C299F35688E5140A48B61C95F004121BF8236201415E5CD45BA41AAB16D4"
+                        + "90B44B9E6D5D92B5B97D24196A58C73F";
+
+        mLocalAddress = IPV6_ADDRESS_LOCAL;
+        mRemoteAddress = IPV6_ADDRESS_REMOTE;
+
+        // Teardown current test network that uses IPv4 address and set up new network with IPv6
+        // address.
+        tearDownTestNetwork();
+        setUpTestNetwork(mLocalAddress);
+
+        // Open IKE Session
+        IkeSession ikeSession = openIkeSessionWithRemoteAddress(mRemoteAddress);
+        performSetupIkeAndFirstChildBlocking(
+                ikeInitResp,
+                1 /* expectedAuthReqPktCnt */,
+                false /* expectedAuthUseEncap */,
+                ikeAuthResp);
+
+        // Local request message ID starts from 2 because there is one IKE_INIT message and a single
+        // IKE_AUTH message.
+        int expectedMsgId = 2;
+
+        verifyIkeSessionSetupBlocking();
+        verifyChildSessionSetupBlocking(
+                mFirstChildSessionCallback,
+                Arrays.asList(TUNNEL_MODE_INBOUND_TS_V6),
+                Arrays.asList(TUNNEL_MODE_OUTBOUND_TS_V6),
+                Arrays.asList(EXPECTED_INTERNAL_LINK_ADDR_V6),
+                Arrays.asList(EXPECTED_DNS_SERVERS_ONE, EXPECTED_DNS_SERVERS_TWO));
+
+        IpSecTransformCallRecord firstTransformRecordA =
+                mFirstChildSessionCallback.awaitNextCreatedIpSecTransform();
+        IpSecTransformCallRecord firstTransformRecordB =
+                mFirstChildSessionCallback.awaitNextCreatedIpSecTransform();
+        verifyCreateIpSecTransformPair(firstTransformRecordA, firstTransformRecordB);
+
+        // Close IKE Session
+        ikeSession.close();
+        performCloseIkeBlocking(expectedMsgId++, false /* expectedUseEncap */, deleteIkeResp);
+        verifyCloseIkeAndChildBlocking(firstTransformRecordA, firstTransformRecordB);
+    }
+
+    @Test
+    public void testIkeSessionKillWithTunnelMode() throws Exception {
+        if (!hasTunnelsFeature()) return;
+
+        // Open IKE Session
+        IkeSession ikeSession = openIkeSessionWithRemoteAddress(mRemoteAddress);
+        performSetupIkeAndFirstChildBlocking(SUCCESS_IKE_INIT_RESP, SUCCESS_IKE_AUTH_RESP);
+
+        ikeSession.kill();
+        mFirstChildSessionCallback.awaitOnClosed();
+        mIkeSessionCallback.awaitOnClosed();
+    }
+
+    @Test
+    public void testIkeInitFail() throws Exception {
+        final String ikeInitFailRespHex =
+                "46B8ECA1E0D72A180000000000000000292022200000000000000024000000080000000E";
+
+        // Open IKE Session
+        IkeSession ikeSession = openIkeSessionWithRemoteAddress(mRemoteAddress);
+        int expectedMsgId = 0;
+        mTunUtils.awaitReqAndInjectResp(
+                IKE_DETERMINISTIC_INITIATOR_SPI,
+                expectedMsgId++,
+                false /* expectedUseEncap */,
+                ikeInitFailRespHex);
+
+        mFirstChildSessionCallback.awaitOnClosed();
+
+        IkeProtocolException protocolException =
+                (IkeProtocolException) mIkeSessionCallback.awaitOnClosedException();
+        assertEquals(ERROR_TYPE_NO_PROPOSAL_CHOSEN, protocolException.getErrorType());
+        assertArrayEquals(EXPECTED_PROTOCOL_ERROR_DATA_NONE, protocolException.getErrorData());
+    }
+
+    @Test
+    public void testIkeAuthHandlesAuthFailNotification() throws Exception {
+        final String ikeInitRespHex =
+                "46B8ECA1E0D72A18CF94CE3159486F002120222000000000000001502200"
+                        + "00300000002C010100040300000C0100000C800E01000300000803000005"
+                        + "0300000802000004000000080400000228000088000200001821AA854691"
+                        + "FA3292DF710F0AC149ACBD0CB421608B8796C1912AF04C5B4B23936FDEC4"
+                        + "7CB640E3EAFB56BBB562825E87AF68B40E4BAB80A49BAD44407450A4195A"
+                        + "1DD54BD99F48D28C9F0FBA315A3401C1C3C4AD55911F514A8DF2D2467C46"
+                        + "A73DDC1452AE81336E0F0D5EC896D2E7A77628AF2F9089F48943399DF216"
+                        + "EFCD2900002418D2B7E4E6AF0FEFF5962CF8D68F7793B1293FEDE13331D4"
+                        + "AB0CE9436C2EE1EC2900001C0000400457BD9AEF5B362A83DD7F3DDAA4A9"
+                        + "9B6B4041DAF32900001C000040055A81893582701E44D4B6729A22FE06DE"
+                        + "82A03A36290000080000402E290000100000402F00020003000400050000"
+                        + "000800004014";
+        final String ikeAuthFailRespHex =
+                "46B8ECA1E0D72A18CF94CE3159486F002E202320000000010000004C2900"
+                        + "00301B9E4C8242D3BE62E7F0A537FE8B92C6EAB7153105DA421DCE43A06D"
+                        + "AB6E4808BAC0CA1DAD6ADD0A126A41BD";
+
+        // Open IKE Session
+        IkeSession ikeSession = openIkeSessionWithRemoteAddress(mRemoteAddress);
+        performSetupIkeAndFirstChildBlocking(ikeInitRespHex, ikeAuthFailRespHex);
+
+        mFirstChildSessionCallback.awaitOnClosed();
+        IkeProtocolException protocolException =
+                (IkeProtocolException) mIkeSessionCallback.awaitOnClosedException();
+        assertEquals(ERROR_TYPE_AUTHENTICATION_FAILED, protocolException.getErrorType());
+        assertArrayEquals(EXPECTED_PROTOCOL_ERROR_DATA_NONE, protocolException.getErrorData());
+    }
+
+    @Test
+    public void testIkeAuthHandlesFirstChildCreationFail() throws Exception {
+        final String ikeInitRespHex =
+                "46B8ECA1E0D72A182B300285DA19E6452120222000000000000001502200"
+                        + "00300000002C010100040300000C0100000C800E01000300000803000005"
+                        + "0300000802000004000000080400000228000088000200005C9DE629981F"
+                        + "DB1FC45DB6CCF15D076C1F51BD9F63C771DC089F05CCDE6247965D15C616"
+                        + "C7B5A62342491715E4D1FEA19326477D24143E8E56AB6AD93F54B19BC32A"
+                        + "44BC0A5B5632E57D0A3C43E466E1547D8E4EF65EA4B864A348161666E229"
+                        + "84975A486251A17C4F096A6D5CF3DB83874B70324A31AA7ADDE2D73BADD8"
+                        + "238029000024CF06260F7C4923295E7C91F2B8479212892DA7A519A0322F"
+                        + "F5B2BF570B92972B2900001C00004004C7ACC2C7D58CF8C9F5E953993AF4"
+                        + "6CAC976635B42900001C00004005B64B190DFE7BDE8B9B1475EDE67B63D6"
+                        + "F1DBBF44290000080000402E290000100000402F00020003000400050000"
+                        + "000800004014";
+        final String ikeAuthCreateChildFailHex =
+                "46B8ECA1E0D72A182B300285DA19E6452E202320000000010000008C2400"
+                        + "0070386FC9CCC67495A17915D0544390A2963A769F4A42C6FA668CEEC07F"
+                        + "EC0C87D681DE34267023DD394F1401B5A563E71002C0CE0928D0ABC0C4570"
+                        + "E39C2EDEF820F870AB71BD70A3F3EB5C96CA294B6D3F01677690DCF9F8CFC"
+                        + "9584650957573502BA83E32F18207A9ADEB1FA";
+
+        // Open IKE Session
+        IkeSession ikeSession = openIkeSessionWithRemoteAddress(mRemoteAddress);
+        performSetupIkeAndFirstChildBlocking(ikeInitRespHex, ikeAuthCreateChildFailHex);
+
+        // Even though the child creation failed, the authentication succeeded, so the IKE Session's
+        // onOpened() callback is still expected
+        verifyIkeSessionSetupBlocking();
+
+        // Verify Child Creation failed
+        IkeProtocolException protocolException =
+                (IkeProtocolException) mFirstChildSessionCallback.awaitOnClosedException();
+        assertEquals(ERROR_TYPE_INTERNAL_ADDRESS_FAILURE, protocolException.getErrorType());
+        assertArrayEquals(EXPECTED_PROTOCOL_ERROR_DATA_NONE, protocolException.getErrorData());
+
+        ikeSession.kill();
+        mIkeSessionCallback.awaitOnClosed();
+    }
+}
diff --git a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionRekeyTest.java b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionRekeyTest.java
new file mode 100644
index 0000000..f954fcd
--- /dev/null
+++ b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionRekeyTest.java
@@ -0,0 +1,265 @@
+/*
+ * 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 android.net.ipsec.ike.cts;
+
+import static com.android.internal.util.HexDump.hexStringToByteArray;
+
+import android.net.InetAddresses;
+import android.net.LinkAddress;
+import android.net.ipsec.ike.IkeFqdnIdentification;
+import android.net.ipsec.ike.IkeSession;
+import android.net.ipsec.ike.IkeSessionParams;
+import android.net.ipsec.ike.IkeTrafficSelector;
+import android.net.ipsec.ike.cts.IkeTunUtils.PortPair;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Explicitly test transport mode Child SA so that devices without FEATURE_IPSEC_TUNNELS can be test
+ * covered. Tunnel mode Child SA setup has been tested in IkeSessionPskTest. Rekeying process is
+ * independent from Child SA mode.
+ */
+@RunWith(AndroidJUnit4.class)
+public class IkeSessionRekeyTest extends IkeSessionTestBase {
+    // This value is align with the test vectors hex that are generated in an IPv4 environment
+    private static final IkeTrafficSelector TRANSPORT_MODE_INBOUND_TS =
+            new IkeTrafficSelector(
+                    MIN_PORT,
+                    MAX_PORT,
+                    InetAddresses.parseNumericAddress("172.58.35.40"),
+                    InetAddresses.parseNumericAddress("172.58.35.40"));
+
+    private IkeSession openIkeSessionWithRemoteAddress(InetAddress remoteAddress) {
+        IkeSessionParams ikeParams =
+                new IkeSessionParams.Builder(sContext)
+                        .setNetwork(mTunNetwork)
+                        .setServerHostname(remoteAddress.getHostAddress())
+                        .addSaProposal(SaProposalTest.buildIkeSaProposalWithNormalModeCipher())
+                        .addSaProposal(SaProposalTest.buildIkeSaProposalWithCombinedModeCipher())
+                        .setLocalIdentification(new IkeFqdnIdentification(LOCAL_HOSTNAME))
+                        .setRemoteIdentification(new IkeFqdnIdentification(REMOTE_HOSTNAME))
+                        .setAuthPsk(IKE_PSK)
+                        .build();
+        return new IkeSession(
+                sContext,
+                ikeParams,
+                buildTransportModeChildParamsWithTs(
+                        TRANSPORT_MODE_INBOUND_TS, TRANSPORT_MODE_OUTBOUND_TS),
+                mUserCbExecutor,
+                mIkeSessionCallback,
+                mFirstChildSessionCallback);
+    }
+
+    private byte[] buildInboundPkt(PortPair outPktSrcDestPortPair, String inboundDataHex)
+            throws Exception {
+        // Build inbound packet by flipping the outbound packet addresses and ports
+        return IkeTunUtils.buildIkePacket(
+                mRemoteAddress,
+                mLocalAddress,
+                outPktSrcDestPortPair.dstPort,
+                outPktSrcDestPortPair.srcPort,
+                true /* useEncap */,
+                hexStringToByteArray(inboundDataHex));
+    }
+
+    @Test
+    public void testRekeyIke() throws Exception {
+        final String ikeInitResp =
+                "46B8ECA1E0D72A1866B5248CF6C7472D21202220000000000000015022000030"
+                        + "0000002C010100040300000C0100000C800E0100030000080300000C03000008"
+                        + "0200000500000008040000022800008800020000920D3E830E7276908209212D"
+                        + "E5A7F2A48706CFEF1BE8CB6E3B173B8B4E0D8C2DC626271FF1B13A88619E569E"
+                        + "7B03C3ED2C127390749CDC7CDC711D0A8611E4457FFCBC4F0981B3288FBF58EA"
+                        + "3E8B70E27E76AE70117FBBCB753660ADDA37EB5EB3A81BED6A374CCB7E132C2A"
+                        + "94BFCE402DC76B19C158B533F6B1F2ABF01ACCC329000024B302CA2FB85B6CF4"
+                        + "02313381246E3C53828D787F6DFEA6BD62D6405254AEE6242900001C00004004"
+                        + "7A1682B06B58596533D00324886EF1F20EF276032900001C00004005BF633E31"
+                        + "F9984B29A62E370BB2770FC09BAEA665290000080000402E290000100000402F"
+                        + "00020003000400050000000800004014";
+        final String ikeAuthResp =
+                "46B8ECA1E0D72A1866B5248CF6C7472D2E20232000000001000000F0240000D4"
+                        + "10166CA8647F56123DE74C17FA5E256043ABF73216C812EE32EE1BB01EAF4A82"
+                        + "DC107AB3ADBFEE0DEA5EEE10BDD5D43178F4C975C7C775D252273BB037283C7F"
+                        + "236FE34A6BCE4833816897075DB2055B9FFD66DFA45A0A89A8F70AFB59431EED"
+                        + "A20602FB614369D12906D3355CF7298A5D25364ABBCC75A9D88E0E6581449FCD"
+                        + "4E361A39E00EFD1FD0A69651F63DB46C12470226AA21BA5EFF48FAF0B6DDF61C"
+                        + "B0A69392CE559495EEDB4D1C1D80688434D225D57210A424C213F7C993D8A456"
+                        + "38153FBD194C5E247B592D1D048DB4C8";
+        final String rekeyIkeCreateReq =
+                "46B8ECA1E0D72A1866B5248CF6C7472D2E202400000000000000013021000114"
+                        + "13743670039E308A8409BA5FD47B67F956B36FEE88AC3B70BB5D789B8218A135"
+                        + "1B3D83E260E87B3EDB1BF064F09D4DC2611AEDBC99951B4B2DE767BD4AA2ACC3"
+                        + "3653549CFC66B75869DF003CDC9A137A9CC27776AD5732B34203E74BE8CA4858"
+                        + "1D5C0D9C9CA52D680EB299B4B21C7FA25FFEE174D57015E0FF2EAED653AAD95C"
+                        + "071ABE269A8C2C9FBC1188E07550EB992F910D4CA9689E44BA66DE0FABB2BDF9"
+                        + "8DD377186DBB25EF9B68B027BB2A27981779D8303D88D7CE860010A42862D50B"
+                        + "1E0DBFD3D27C36F14809D7F493B2B96A65534CF98B0C32AD5219AD77F681AC04"
+                        + "9D5CB89A0230A91A243FA7F16251B0D9B4B65E7330BEEAC9663EF4578991EAC8"
+                        + "46C19EBB726E7D113F1D0D601102C05E";
+        final String rekeyIkeDeleteReq =
+                "46B8ECA1E0D72A1866B5248CF6C7472D2E20250000000001000000502A000034"
+                        + "02E40C0C7B1ED977729F705BB9B643FAC513A1070A6EB28ECD2AEA8A441ADC05"
+                        + "7841382A7967BBF116AE52496590B2AD";
+        final String deleteIkeReq =
+                "7D3DEDC65407D1FC9361C8CF8C47162A2E20250800000000000000502A000034"
+                        + "201915C9E4E9173AA9EE79F3E02FE2D4954B22085C66D164762C34D347C16E9F"
+                        + "FC5F7F114428C54D8D915860C57B1BC1";
+        final long newIkeDeterministicInitSpi = Long.parseLong("7D3DEDC65407D1FC", 16);
+
+        // Open IKE Session
+        IkeSession ikeSession = openIkeSessionWithRemoteAddress(mRemoteAddress);
+        PortPair localRemotePorts = performSetupIkeAndFirstChildBlocking(ikeInitResp, ikeAuthResp);
+
+        // Local request message ID starts from 2 because there is one IKE_INIT message and a single
+        // IKE_AUTH message.
+        int expectedReqMsgId = 2;
+        int expectedRespMsgId = 0;
+
+        verifyIkeSessionSetupBlocking();
+        verifyChildSessionSetupBlocking(
+                mFirstChildSessionCallback,
+                Arrays.asList(TRANSPORT_MODE_INBOUND_TS),
+                Arrays.asList(TRANSPORT_MODE_OUTBOUND_TS),
+                new ArrayList<LinkAddress>());
+        IpSecTransformCallRecord firstTransformRecordA =
+                mFirstChildSessionCallback.awaitNextCreatedIpSecTransform();
+        IpSecTransformCallRecord firstTransformRecordB =
+                mFirstChildSessionCallback.awaitNextCreatedIpSecTransform();
+        verifyCreateIpSecTransformPair(firstTransformRecordA, firstTransformRecordB);
+
+        // Inject rekey IKE requests
+        mTunUtils.injectPacket(buildInboundPkt(localRemotePorts, rekeyIkeCreateReq));
+        mTunUtils.awaitResp(
+                IKE_DETERMINISTIC_INITIATOR_SPI, expectedRespMsgId++, true /* expectedUseEncap */);
+        mTunUtils.injectPacket(buildInboundPkt(localRemotePorts, rekeyIkeDeleteReq));
+        mTunUtils.awaitResp(
+                IKE_DETERMINISTIC_INITIATOR_SPI, expectedRespMsgId++, true /* expectedUseEncap */);
+
+        // IKE has been rekeyed, reset message IDs
+        expectedReqMsgId = 0;
+        expectedRespMsgId = 0;
+
+        // Inject delete IKE request
+        mTunUtils.injectPacket(buildInboundPkt(localRemotePorts, deleteIkeReq));
+        mTunUtils.awaitResp(
+                newIkeDeterministicInitSpi, expectedRespMsgId++, true /* expectedUseEncap */);
+
+        verifyDeleteIpSecTransformPair(
+                mFirstChildSessionCallback, firstTransformRecordA, firstTransformRecordB);
+        mFirstChildSessionCallback.awaitOnClosed();
+        mIkeSessionCallback.awaitOnClosed();
+    }
+
+    @Test
+    public void testRekeyTransportModeChildSa() throws Exception {
+        final String ikeInitResp =
+                "46B8ECA1E0D72A18CECD871146CF83A121202220000000000000015022000030"
+                        + "0000002C010100040300000C0100000C800E0100030000080300000C03000008"
+                        + "0200000500000008040000022800008800020000C4904458957746BCF1C12972"
+                        + "1D4E19EB8A584F78DE673053396D167CE0F34552DBC69BA63FE7C673B4CF4A99"
+                        + "62481518EE985357876E8C47BAAA0DBE9C40AE47B12E52165874703586E8F786"
+                        + "045F72EEEB238C5D1823352BED44B71B3214609276ADC0B3D42DAC820168C4E2"
+                        + "660730DAAC92492403288805EBB9053F1AB060DA290000242D9364ACB93519FF"
+                        + "8F8B019BAA43A40D699F59714B327B8382216EF427ED52282900001C00004004"
+                        + "06D91438A0D6B734E152F76F5CC55A72A2E38A0A2900001C000040052EFF78B3"
+                        + "55B37F3CE75AFF26C721B050F892C0D6290000080000402E290000100000402F"
+                        + "00020003000400050000000800004014";
+        final String ikeAuthResp =
+                "46B8ECA1E0D72A18CECD871146CF83A12E20232000000001000000F0240000D4"
+                        + "A17BC258BA2714CF536663639DD5F665A60C75E93557CD5141990A8CEEDD2017"
+                        + "93F5B181C8569FBCD6C2A00198EC2B62D42BEFAC016B8B6BF6A7BC9CEDE3413A"
+                        + "6C495A6B8EC941864DC3E08F57D015EA6520C4B05884960B85478FCA53DA5F17"
+                        + "9628BB1097DA77461C71837207A9EB80720B3E6E661816EE4E14AC995B5E8441"
+                        + "A4C3F9097CC148142BA300076C94A23EC4ADE82B1DD2B121F7E9102860A8C3BF"
+                        + "58DDC207285A3176E924C44DE820322524E1AA438EFDFBA781B36084AED80846"
+                        + "3B77FCED9682B6E4E476408EF3F1037E";
+        final String rekeyChildCreateReq =
+                "46B8ECA1E0D72A18CECD871146CF83A12E202400000000000000015029000134"
+                        + "319D74B6B155B86942143CEC1D29D21F073F24B7BEDC9BFE0F0FDD8BDB5458C0"
+                        + "8DB93506E1A43DD0640FE7370C97F9B34FF4EC9B2DB7257A87B75632301FB68A"
+                        + "86B54871249534CA3D01C9BEB127B669F46470E1C8AAF72574C3CEEC15B901CF"
+                        + "5A0D6ADAE59C3CA64AC8C86689C860FAF9500E608DFE63F2DCD30510FD6FFCD5"
+                        + "A50838574132FD1D069BCACD4C7BAF45C9B1A7689FAD132E3F56DBCFAF905A8C"
+                        + "4145D4BA1B74A54762F8F43308D94DE05649C49D885121CE30681D51AC1E3E68"
+                        + "AB82F9A19B99579AFE257F32DBD1037814DA577379E4F42DEDAC84502E49C933"
+                        + "9EA83F6F5DB4401B660CB1681B023B8603D205DFDD1DE86AD8DE22B6B754F30D"
+                        + "05EAE81A709C2CEE81386133DC3DC7B5EF8F166E48E54A0722DD0C64F4D00638"
+                        + "40F272144C47F6ECED72A248180645DB";
+        final String rekeyChildDeleteReq =
+                "46B8ECA1E0D72A18CECD871146CF83A12E20250000000001000000502A000034"
+                        + "02D98DAF0432EBD991CA4F2D89C1E0EFABC6E91A3327A85D8914FB2F1485BE1B"
+                        + "8D3415D548F7CE0DC4224E7E9D0D3355";
+        final String deleteIkeReq =
+                "46B8ECA1E0D72A18CECD871146CF83A12E20250000000002000000502A000034"
+                        + "095041F4026B4634F04B0AB4F9349484F7BE9AEF03E3733EEE293330043B75D2"
+                        + "ABF5F965ED51127629585E1B1BBA787F";
+
+        // Open IKE Session
+        IkeSession ikeSession = openIkeSessionWithRemoteAddress(mRemoteAddress);
+        PortPair localRemotePorts = performSetupIkeAndFirstChildBlocking(ikeInitResp, ikeAuthResp);
+
+        // IKE INIT and IKE AUTH takes two exchanges. Local request message ID starts from 2
+        int expectedReqMsgId = 2;
+        int expectedRespMsgId = 0;
+
+        verifyIkeSessionSetupBlocking();
+        verifyChildSessionSetupBlocking(
+                mFirstChildSessionCallback,
+                Arrays.asList(TRANSPORT_MODE_INBOUND_TS),
+                Arrays.asList(TRANSPORT_MODE_OUTBOUND_TS),
+                new ArrayList<LinkAddress>());
+        IpSecTransformCallRecord oldTransformRecordA =
+                mFirstChildSessionCallback.awaitNextCreatedIpSecTransform();
+        IpSecTransformCallRecord oldTransformRecordB =
+                mFirstChildSessionCallback.awaitNextCreatedIpSecTransform();
+        verifyCreateIpSecTransformPair(oldTransformRecordA, oldTransformRecordB);
+
+        // Inject rekey Child requests
+        mTunUtils.injectPacket(buildInboundPkt(localRemotePorts, rekeyChildCreateReq));
+        mTunUtils.awaitResp(
+                IKE_DETERMINISTIC_INITIATOR_SPI, expectedRespMsgId++, true /* expectedUseEncap */);
+        mTunUtils.injectPacket(buildInboundPkt(localRemotePorts, rekeyChildDeleteReq));
+        mTunUtils.awaitResp(
+                IKE_DETERMINISTIC_INITIATOR_SPI, expectedRespMsgId++, true /* expectedUseEncap */);
+
+        // Verify IpSecTransforms are renewed
+        IpSecTransformCallRecord newTransformRecordA =
+                mFirstChildSessionCallback.awaitNextCreatedIpSecTransform();
+        IpSecTransformCallRecord newTransformRecordB =
+                mFirstChildSessionCallback.awaitNextCreatedIpSecTransform();
+        verifyCreateIpSecTransformPair(newTransformRecordA, newTransformRecordB);
+        verifyDeleteIpSecTransformPair(
+                mFirstChildSessionCallback, oldTransformRecordA, oldTransformRecordB);
+
+        // Inject delete IKE request
+        mTunUtils.injectPacket(buildInboundPkt(localRemotePorts, deleteIkeReq));
+        mTunUtils.awaitResp(
+                IKE_DETERMINISTIC_INITIATOR_SPI, expectedRespMsgId++, true /* expectedUseEncap */);
+
+        verifyDeleteIpSecTransformPair(
+                mFirstChildSessionCallback, newTransformRecordA, newTransformRecordB);
+        mFirstChildSessionCallback.awaitOnClosed();
+        mIkeSessionCallback.awaitOnClosed();
+    }
+}
diff --git a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionTestBase.java b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionTestBase.java
new file mode 100644
index 0000000..2458b25
--- /dev/null
+++ b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionTestBase.java
@@ -0,0 +1,591 @@
+/*
+ * 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
+ *
+ * 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.ipsec.ike.cts;
+
+import static android.net.ipsec.ike.IkeSessionConfiguration.EXTENSION_TYPE_FRAGMENTATION;
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.annotation.NonNull;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.InetAddresses;
+import android.net.IpSecManager;
+import android.net.IpSecTransform;
+import android.net.LinkAddress;
+import android.net.Network;
+import android.net.TestNetworkInterface;
+import android.net.TestNetworkManager;
+import android.net.annotations.PolicyDirection;
+import android.net.ipsec.ike.ChildSessionCallback;
+import android.net.ipsec.ike.ChildSessionConfiguration;
+import android.net.ipsec.ike.IkeSessionCallback;
+import android.net.ipsec.ike.IkeSessionConfiguration;
+import android.net.ipsec.ike.IkeSessionConnectionInfo;
+import android.net.ipsec.ike.IkeTrafficSelector;
+import android.net.ipsec.ike.TransportModeChildSessionParams;
+import android.net.ipsec.ike.TunnelModeChildSessionParams;
+import android.net.ipsec.ike.cts.IkeTunUtils.PortPair;
+import android.net.ipsec.ike.cts.TestNetworkUtils.TestNetworkCallback;
+import android.net.ipsec.ike.exceptions.IkeException;
+import android.net.ipsec.ike.exceptions.IkeProtocolException;
+import android.os.Binder;
+import android.os.ParcelFileDescriptor;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.compatibility.common.util.SystemUtil;
+import com.android.testutils.ArrayTrackRecord;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.runner.RunWith;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Package private base class for testing IkeSessionParams and IKE exchanges.
+ *
+ * <p>Subclasses MUST explicitly call #setUpTestNetwork and #tearDownTestNetwork to be able to use
+ * the test network
+ *
+ * <p>All IKE Sessions running in test mode will generate SPIs deterministically. That is to say
+ * each IKE Session will always generate the same IKE INIT SPI and test vectors are generated based
+ * on this deterministic IKE SPI. Each test will use different local and remote addresses to avoid
+ * the case that the next test try to allocate the same SPI before the previous test has released
+ * it, since SPI resources are not released in testing thread. Similarly, each test MUST use
+ * different Network instances to avoid sharing the same IkeSocket and hitting IKE SPI collision.
+ */
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "MANAGE_TEST_NETWORKS permission can't be granted to instant apps")
+abstract class IkeSessionTestBase extends IkeTestBase {
+    // Package-wide common expected results that will be shared by all IKE/Child SA creation tests
+    static final String EXPECTED_REMOTE_APP_VERSION_EMPTY = "";
+    static final byte[] EXPECTED_PROTOCOL_ERROR_DATA_NONE = new byte[0];
+
+    static final InetAddress EXPECTED_DNS_SERVERS_ONE =
+            InetAddresses.parseNumericAddress("8.8.8.8");
+    static final InetAddress EXPECTED_DNS_SERVERS_TWO =
+            InetAddresses.parseNumericAddress("8.8.4.4");
+
+    static final InetAddress EXPECTED_INTERNAL_ADDR =
+            InetAddresses.parseNumericAddress("198.51.100.10");
+    static final LinkAddress EXPECTED_INTERNAL_LINK_ADDR =
+            new LinkAddress(EXPECTED_INTERNAL_ADDR, IP4_PREFIX_LEN);
+    static final InetAddress EXPECTED_INTERNAL_ADDR_V6 =
+            InetAddresses.parseNumericAddress("2001:db8::2");
+    static final LinkAddress EXPECTED_INTERNAL_LINK_ADDR_V6 =
+            new LinkAddress(EXPECTED_INTERNAL_ADDR_V6, IP6_PREFIX_LEN);
+
+    static final IkeTrafficSelector TUNNEL_MODE_INBOUND_TS =
+            new IkeTrafficSelector(
+                    MIN_PORT, MAX_PORT, EXPECTED_INTERNAL_ADDR, EXPECTED_INTERNAL_ADDR);
+    static final IkeTrafficSelector TUNNEL_MODE_OUTBOUND_TS = DEFAULT_V4_TS;
+    static final IkeTrafficSelector TUNNEL_MODE_INBOUND_TS_V6 =
+            new IkeTrafficSelector(
+                    MIN_PORT, MAX_PORT, EXPECTED_INTERNAL_ADDR_V6, EXPECTED_INTERNAL_ADDR_V6);
+    static final IkeTrafficSelector TUNNEL_MODE_OUTBOUND_TS_V6 = DEFAULT_V6_TS;
+
+    // This value is align with the test vectors hex that are generated in an IPv4 environment
+    static final IkeTrafficSelector TRANSPORT_MODE_OUTBOUND_TS =
+            new IkeTrafficSelector(
+                    MIN_PORT,
+                    MAX_PORT,
+                    InetAddresses.parseNumericAddress("10.138.0.2"),
+                    InetAddresses.parseNumericAddress("10.138.0.2"));
+
+    static final long IKE_DETERMINISTIC_INITIATOR_SPI = Long.parseLong("46B8ECA1E0D72A18", 16);
+
+    // Static state to reduce setup/teardown
+    static Context sContext = InstrumentationRegistry.getContext();
+    static ConnectivityManager sCM =
+            (ConnectivityManager) sContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+    static TestNetworkManager sTNM;
+
+    private static final int TIMEOUT_MS = 500;
+
+    // Constants to be used for providing different IP addresses for each tests
+    private static final byte IP_ADDR_LAST_BYTE_MAX = (byte) 100;
+    private static final byte[] INITIAL_AVAILABLE_IP4_ADDR_LOCAL =
+            InetAddresses.parseNumericAddress("192.0.2.1").getAddress();
+    private static final byte[] INITIAL_AVAILABLE_IP4_ADDR_REMOTE =
+            InetAddresses.parseNumericAddress("198.51.100.1").getAddress();
+    private static final byte[] NEXT_AVAILABLE_IP4_ADDR_LOCAL = INITIAL_AVAILABLE_IP4_ADDR_LOCAL;
+    private static final byte[] NEXT_AVAILABLE_IP4_ADDR_REMOTE = INITIAL_AVAILABLE_IP4_ADDR_REMOTE;
+
+    ParcelFileDescriptor mTunFd;
+    TestNetworkCallback mTunNetworkCallback;
+    Network mTunNetwork;
+    IkeTunUtils mTunUtils;
+
+    InetAddress mLocalAddress;
+    InetAddress mRemoteAddress;
+
+    Executor mUserCbExecutor;
+    TestIkeSessionCallback mIkeSessionCallback;
+    TestChildSessionCallback mFirstChildSessionCallback;
+
+    // This method is guaranteed to run in subclasses and will run before subclasses' @BeforeClass
+    // methods.
+    @BeforeClass
+    public static void setUpPermissionBeforeClass() throws Exception {
+        InstrumentationRegistry.getInstrumentation()
+                .getUiAutomation()
+                .adoptShellPermissionIdentity();
+        sTNM = sContext.getSystemService(TestNetworkManager.class);
+    }
+
+    // This method is guaranteed to run in subclasses and will run after subclasses' @AfterClass
+    // methods.
+    @AfterClass
+    public static void tearDownPermissionAfterClass() throws Exception {
+        InstrumentationRegistry.getInstrumentation()
+                .getUiAutomation()
+                .dropShellPermissionIdentity();
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        mLocalAddress = getNextAvailableIpv4AddressLocal();
+        mRemoteAddress = getNextAvailableIpv4AddressRemote();
+        setUpTestNetwork(mLocalAddress);
+
+        mUserCbExecutor = Executors.newSingleThreadExecutor();
+        mIkeSessionCallback = new TestIkeSessionCallback();
+        mFirstChildSessionCallback = new TestChildSessionCallback();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        tearDownTestNetwork();
+    }
+
+    void setUpTestNetwork(InetAddress localAddr) throws Exception {
+        int prefixLen = localAddr instanceof Inet4Address ? IP4_PREFIX_LEN : IP6_PREFIX_LEN;
+
+        TestNetworkInterface testIface =
+                sTNM.createTunInterface(new LinkAddress[] {new LinkAddress(localAddr, prefixLen)});
+
+        mTunFd = testIface.getFileDescriptor();
+        mTunNetworkCallback =
+                TestNetworkUtils.setupAndGetTestNetwork(
+                        sCM, sTNM, testIface.getInterfaceName(), new Binder());
+        mTunNetwork = mTunNetworkCallback.getNetworkBlocking();
+        mTunUtils = new IkeTunUtils(mTunFd);
+    }
+
+    void tearDownTestNetwork() throws Exception {
+        sCM.unregisterNetworkCallback(mTunNetworkCallback);
+
+        sTNM.teardownTestNetwork(mTunNetwork);
+        mTunFd.close();
+    }
+
+    static void setAppOp(int appop, boolean allow) {
+        String opName = AppOpsManager.opToName(appop);
+        for (String pkg : new String[] {"com.android.shell", sContext.getPackageName()}) {
+            String cmd =
+                    String.format(
+                            "appops set %s %s %s",
+                            pkg, // Package name
+                            opName, // Appop
+                            (allow ? "allow" : "deny")); // Action
+
+            SystemUtil.runShellCommand(cmd);
+        }
+    }
+
+    Inet4Address getNextAvailableIpv4AddressLocal() throws Exception {
+        return (Inet4Address)
+                getNextAvailableAddress(
+                        NEXT_AVAILABLE_IP4_ADDR_LOCAL,
+                        INITIAL_AVAILABLE_IP4_ADDR_LOCAL,
+                        false /* isIp6 */);
+    }
+
+    Inet4Address getNextAvailableIpv4AddressRemote() throws Exception {
+        return (Inet4Address)
+                getNextAvailableAddress(
+                        NEXT_AVAILABLE_IP4_ADDR_REMOTE,
+                        INITIAL_AVAILABLE_IP4_ADDR_REMOTE,
+                        false /* isIp6 */);
+    }
+
+    InetAddress getNextAvailableAddress(
+            byte[] nextAddressBytes, byte[] initialAddressBytes, boolean isIp6) throws Exception {
+        int addressLen = isIp6 ? IP6_ADDRESS_LEN : IP4_ADDRESS_LEN;
+
+        synchronized (nextAddressBytes) {
+            if (nextAddressBytes[addressLen - 1] == IP_ADDR_LAST_BYTE_MAX) {
+                resetNextAvailableAddress(nextAddressBytes, initialAddressBytes);
+            }
+
+            InetAddress address = InetAddress.getByAddress(nextAddressBytes);
+            nextAddressBytes[addressLen - 1]++;
+            return address;
+        }
+    }
+
+    private void resetNextAvailableAddress(byte[] nextAddressBytes, byte[] initialAddressBytes) {
+        synchronized (nextAddressBytes) {
+            System.arraycopy(
+                    nextAddressBytes, 0, initialAddressBytes, 0, initialAddressBytes.length);
+        }
+    }
+
+    TransportModeChildSessionParams buildTransportModeChildParamsWithTs(
+            IkeTrafficSelector inboundTs, IkeTrafficSelector outboundTs) {
+        return new TransportModeChildSessionParams.Builder()
+                .addSaProposal(SaProposalTest.buildChildSaProposalWithCombinedModeCipher())
+                .addSaProposal(SaProposalTest.buildChildSaProposalWithNormalModeCipher())
+                .addInboundTrafficSelectors(inboundTs)
+                .addOutboundTrafficSelectors(outboundTs)
+                .build();
+    }
+
+    TunnelModeChildSessionParams buildTunnelModeChildSessionParams() {
+        return new TunnelModeChildSessionParams.Builder()
+                .addSaProposal(SaProposalTest.buildChildSaProposalWithNormalModeCipher())
+                .addSaProposal(SaProposalTest.buildChildSaProposalWithCombinedModeCipher())
+                .addInternalAddressRequest(AF_INET)
+                .addInternalAddressRequest(AF_INET6)
+                .build();
+    }
+
+    PortPair performSetupIkeAndFirstChildBlocking(String ikeInitRespHex, String... ikeAuthRespHexes)
+            throws Exception {
+        return performSetupIkeAndFirstChildBlocking(
+                ikeInitRespHex,
+                1 /* expectedAuthReqPktCnt */,
+                true /*expectedAuthUseEncap*/,
+                ikeAuthRespHexes);
+    }
+
+    PortPair performSetupIkeAndFirstChildBlocking(
+            String ikeInitRespHex, boolean expectedAuthUseEncap, String... ikeAuthRespHexes)
+            throws Exception {
+        return performSetupIkeAndFirstChildBlocking(
+                ikeInitRespHex,
+                1 /* expectedAuthReqPktCnt */,
+                expectedAuthUseEncap,
+                ikeAuthRespHexes);
+    }
+
+    PortPair performSetupIkeAndFirstChildBlocking(
+            String ikeInitRespHex,
+            int expectedAuthReqPktCnt,
+            boolean expectedAuthUseEncap,
+            String... ikeAuthRespHexes)
+            throws Exception {
+        mTunUtils.awaitReqAndInjectResp(
+                IKE_DETERMINISTIC_INITIATOR_SPI,
+                0 /* expectedMsgId */,
+                false /* expectedUseEncap */,
+                ikeInitRespHex);
+
+        byte[] ikeAuthReqPkt =
+                mTunUtils
+                        .awaitReqAndInjectResp(
+                                IKE_DETERMINISTIC_INITIATOR_SPI,
+                                1 /* expectedMsgId */,
+                                expectedAuthUseEncap,
+                                expectedAuthReqPktCnt,
+                                ikeAuthRespHexes)
+                        .get(0);
+        return IkeTunUtils.getSrcDestPortPair(ikeAuthReqPkt);
+    }
+
+    void performCloseIkeBlocking(int expectedMsgId, String deleteIkeRespHex) throws Exception {
+        performCloseIkeBlocking(expectedMsgId, true /* expectedUseEncap*/, deleteIkeRespHex);
+    }
+
+    void performCloseIkeBlocking(
+            int expectedMsgId, boolean expectedUseEncap, String deleteIkeRespHex) throws Exception {
+        mTunUtils.awaitReqAndInjectResp(
+                IKE_DETERMINISTIC_INITIATOR_SPI, expectedMsgId, expectedUseEncap, deleteIkeRespHex);
+    }
+
+    /** Testing callback that allows caller to block current thread until a method get called */
+    static class TestIkeSessionCallback implements IkeSessionCallback {
+        private CompletableFuture<IkeSessionConfiguration> mFutureIkeConfig =
+                new CompletableFuture<>();
+        private CompletableFuture<Boolean> mFutureOnClosedCall = new CompletableFuture<>();
+        private CompletableFuture<IkeException> mFutureOnClosedException =
+                new CompletableFuture<>();
+
+        private int mOnErrorExceptionsCount = 0;
+        private ArrayTrackRecord<IkeProtocolException> mOnErrorExceptionsTrackRecord =
+                new ArrayTrackRecord<>();
+
+        @Override
+        public void onOpened(@NonNull IkeSessionConfiguration sessionConfiguration) {
+            mFutureIkeConfig.complete(sessionConfiguration);
+        }
+
+        @Override
+        public void onClosed() {
+            mFutureOnClosedCall.complete(true /* unused */);
+        }
+
+        @Override
+        public void onClosedExceptionally(@NonNull IkeException exception) {
+            mFutureOnClosedException.complete(exception);
+        }
+
+        @Override
+        public void onError(@NonNull IkeProtocolException exception) {
+            mOnErrorExceptionsTrackRecord.add(exception);
+        }
+
+        public IkeSessionConfiguration awaitIkeConfig() throws Exception {
+            return mFutureIkeConfig.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        }
+
+        public IkeException awaitOnClosedException() throws Exception {
+            return mFutureOnClosedException.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        }
+
+        public IkeProtocolException awaitNextOnErrorException() {
+            return mOnErrorExceptionsTrackRecord.poll(
+                    (long) TIMEOUT_MS,
+                    mOnErrorExceptionsCount++,
+                    (transform) -> {
+                        return true;
+                    });
+        }
+
+        public void awaitOnClosed() throws Exception {
+            mFutureOnClosedCall.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        }
+    }
+
+    /** Testing callback that allows caller to block current thread until a method get called */
+    static class TestChildSessionCallback implements ChildSessionCallback {
+        private CompletableFuture<ChildSessionConfiguration> mFutureChildConfig =
+                new CompletableFuture<>();
+        private CompletableFuture<Boolean> mFutureOnClosedCall = new CompletableFuture<>();
+        private CompletableFuture<IkeException> mFutureOnClosedException =
+                new CompletableFuture<>();
+
+        private int mCreatedIpSecTransformCount = 0;
+        private int mDeletedIpSecTransformCount = 0;
+        private ArrayTrackRecord<IpSecTransformCallRecord> mCreatedIpSecTransformsTrackRecord =
+                new ArrayTrackRecord<>();
+        private ArrayTrackRecord<IpSecTransformCallRecord> mDeletedIpSecTransformsTrackRecord =
+                new ArrayTrackRecord<>();
+
+        @Override
+        public void onOpened(@NonNull ChildSessionConfiguration sessionConfiguration) {
+            mFutureChildConfig.complete(sessionConfiguration);
+        }
+
+        @Override
+        public void onClosed() {
+            mFutureOnClosedCall.complete(true /* unused */);
+        }
+
+        @Override
+        public void onClosedExceptionally(@NonNull IkeException exception) {
+            mFutureOnClosedException.complete(exception);
+        }
+
+        @Override
+        public void onIpSecTransformCreated(@NonNull IpSecTransform ipSecTransform, int direction) {
+            mCreatedIpSecTransformsTrackRecord.add(
+                    new IpSecTransformCallRecord(ipSecTransform, direction));
+        }
+
+        @Override
+        public void onIpSecTransformDeleted(@NonNull IpSecTransform ipSecTransform, int direction) {
+            mDeletedIpSecTransformsTrackRecord.add(
+                    new IpSecTransformCallRecord(ipSecTransform, direction));
+        }
+
+        public ChildSessionConfiguration awaitChildConfig() throws Exception {
+            return mFutureChildConfig.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        }
+
+        public IkeException awaitOnClosedException() throws Exception {
+            return mFutureOnClosedException.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        }
+
+        public IpSecTransformCallRecord awaitNextCreatedIpSecTransform() {
+            return mCreatedIpSecTransformsTrackRecord.poll(
+                    (long) TIMEOUT_MS,
+                    mCreatedIpSecTransformCount++,
+                    (transform) -> {
+                        return true;
+                    });
+        }
+
+        public IpSecTransformCallRecord awaitNextDeletedIpSecTransform() {
+            return mDeletedIpSecTransformsTrackRecord.poll(
+                    (long) TIMEOUT_MS,
+                    mDeletedIpSecTransformCount++,
+                    (transform) -> {
+                        return true;
+                    });
+        }
+
+        public void awaitOnClosed() throws Exception {
+            mFutureOnClosedCall.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        }
+    }
+
+    /**
+     * This class represents a created or deleted IpSecTransfrom that is provided by
+     * ChildSessionCallback
+     */
+    static class IpSecTransformCallRecord {
+        public final IpSecTransform ipSecTransform;
+        public final int direction;
+
+        IpSecTransformCallRecord(IpSecTransform ipSecTransform, @PolicyDirection int direction) {
+            this.ipSecTransform = ipSecTransform;
+            this.direction = direction;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(ipSecTransform, direction);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof IpSecTransformCallRecord)) return false;
+
+            IpSecTransformCallRecord record = (IpSecTransformCallRecord) o;
+            return ipSecTransform.equals(record.ipSecTransform) && direction == record.direction;
+        }
+    }
+
+    void verifyIkeSessionSetupBlocking() throws Exception {
+        IkeSessionConfiguration ikeConfig = mIkeSessionCallback.awaitIkeConfig();
+        assertNotNull(ikeConfig);
+        assertEquals(EXPECTED_REMOTE_APP_VERSION_EMPTY, ikeConfig.getRemoteApplicationVersion());
+        assertTrue(ikeConfig.getRemoteVendorIds().isEmpty());
+        assertTrue(ikeConfig.getPcscfServers().isEmpty());
+        assertTrue(ikeConfig.isIkeExtensionEnabled(EXTENSION_TYPE_FRAGMENTATION));
+
+        IkeSessionConnectionInfo ikeConnectInfo = ikeConfig.getIkeSessionConnectionInfo();
+        assertNotNull(ikeConnectInfo);
+        assertEquals(mLocalAddress, ikeConnectInfo.getLocalAddress());
+        assertEquals(mRemoteAddress, ikeConnectInfo.getRemoteAddress());
+        assertEquals(mTunNetwork, ikeConnectInfo.getNetwork());
+    }
+
+    void verifyChildSessionSetupBlocking(
+            TestChildSessionCallback childCallback,
+            List<IkeTrafficSelector> expectedInboundTs,
+            List<IkeTrafficSelector> expectedOutboundTs,
+            List<LinkAddress> expectedInternalAddresses)
+            throws Exception {
+        verifyChildSessionSetupBlocking(
+                childCallback,
+                expectedInboundTs,
+                expectedOutboundTs,
+                expectedInternalAddresses,
+                new ArrayList<InetAddress>() /* expectedDnsServers */);
+    }
+
+    void verifyChildSessionSetupBlocking(
+            TestChildSessionCallback childCallback,
+            List<IkeTrafficSelector> expectedInboundTs,
+            List<IkeTrafficSelector> expectedOutboundTs,
+            List<LinkAddress> expectedInternalAddresses,
+            List<InetAddress> expectedDnsServers)
+            throws Exception {
+        ChildSessionConfiguration childConfig = childCallback.awaitChildConfig();
+        assertNotNull(childConfig);
+        assertEquals(expectedInboundTs, childConfig.getInboundTrafficSelectors());
+        assertEquals(expectedOutboundTs, childConfig.getOutboundTrafficSelectors());
+        assertEquals(expectedInternalAddresses, childConfig.getInternalAddresses());
+        assertEquals(expectedDnsServers, childConfig.getInternalDnsServers());
+        assertTrue(childConfig.getInternalSubnets().isEmpty());
+        assertTrue(childConfig.getInternalDhcpServers().isEmpty());
+    }
+
+    void verifyCloseIkeAndChildBlocking(
+            IpSecTransformCallRecord expectedTransformRecordA,
+            IpSecTransformCallRecord expectedTransformRecordB)
+            throws Exception {
+        verifyDeleteIpSecTransformPair(
+                mFirstChildSessionCallback, expectedTransformRecordA, expectedTransformRecordB);
+        mFirstChildSessionCallback.awaitOnClosed();
+        mIkeSessionCallback.awaitOnClosed();
+    }
+
+    static void verifyCreateIpSecTransformPair(
+            IpSecTransformCallRecord transformRecordA, IpSecTransformCallRecord transformRecordB) {
+        IpSecTransform transformA = transformRecordA.ipSecTransform;
+        IpSecTransform transformB = transformRecordB.ipSecTransform;
+
+        assertNotNull(transformA);
+        assertNotNull(transformB);
+
+        Set<Integer> expectedDirections = new HashSet<>();
+        expectedDirections.add(IpSecManager.DIRECTION_IN);
+        expectedDirections.add(IpSecManager.DIRECTION_OUT);
+
+        Set<Integer> resultDirections = new HashSet<>();
+        resultDirections.add(transformRecordA.direction);
+        resultDirections.add(transformRecordB.direction);
+
+        assertEquals(expectedDirections, resultDirections);
+    }
+
+    static void verifyDeleteIpSecTransformPair(
+            TestChildSessionCallback childCb,
+            IpSecTransformCallRecord expectedTransformRecordA,
+            IpSecTransformCallRecord expectedTransformRecordB) {
+        Set<IpSecTransformCallRecord> expectedTransforms = new HashSet<>();
+        expectedTransforms.add(expectedTransformRecordA);
+        expectedTransforms.add(expectedTransformRecordB);
+
+        Set<IpSecTransformCallRecord> resultTransforms = new HashSet<>();
+        resultTransforms.add(childCb.awaitNextDeletedIpSecTransform());
+        resultTransforms.add(childCb.awaitNextDeletedIpSecTransform());
+
+        assertEquals(expectedTransforms, resultTransforms);
+    }
+
+    /** Package private method to check if device has IPsec tunnels feature */
+    static boolean hasTunnelsFeature() {
+        return sContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS);
+    }
+
+    // TODO(b/148689509): Verify hostname based creation
+}
diff --git a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeTestBase.java b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeTestBase.java
index bc2bec6..c70e537 100644
--- a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeTestBase.java
+++ b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeTestBase.java
@@ -31,13 +31,15 @@
 
 /** Shared parameters and util methods for testing different components of IKE */
 abstract class IkeTestBase {
-    private static final int MIN_PORT = 0;
-    private static final int MAX_PORT = 65535;
+    static final int MIN_PORT = 0;
+    static final int MAX_PORT = 65535;
     private static final int INBOUND_TS_START_PORT = MIN_PORT;
     private static final int INBOUND_TS_END_PORT = 65520;
     private static final int OUTBOUND_TS_START_PORT = 16;
     private static final int OUTBOUND_TS_END_PORT = MAX_PORT;
 
+    static final int IP4_ADDRESS_LEN = 4;
+    static final int IP6_ADDRESS_LEN = 16;
     static final int IP4_PREFIX_LEN = 32;
     static final int IP6_PREFIX_LEN = 64;
 
@@ -52,7 +54,7 @@
     static final int SUB_ID = 1;
     static final byte[] EAP_IDENTITY = "test@android.net".getBytes();
     static final String NETWORK_NAME = "android.net";
-    static final String EAP_MSCHAPV2_USERNAME = "username";
+    static final String EAP_MSCHAPV2_USERNAME = "mschapv2user";
     static final String EAP_MSCHAPV2_PASSWORD = "password";
 
     static final Inet4Address IPV4_ADDRESS_LOCAL =
diff --git a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeTunUtils.java b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeTunUtils.java
new file mode 100644
index 0000000..41cbf0b
--- /dev/null
+++ b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeTunUtils.java
@@ -0,0 +1,377 @@
+/*
+ * 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 android.net.ipsec.ike.cts;
+
+import static android.net.ipsec.ike.cts.PacketUtils.BytePayload;
+import static android.net.ipsec.ike.cts.PacketUtils.IP4_HDRLEN;
+import static android.net.ipsec.ike.cts.PacketUtils.IP6_HDRLEN;
+import static android.net.ipsec.ike.cts.PacketUtils.Ip4Header;
+import static android.net.ipsec.ike.cts.PacketUtils.Ip6Header;
+import static android.net.ipsec.ike.cts.PacketUtils.IpHeader;
+import static android.net.ipsec.ike.cts.PacketUtils.Payload;
+import static android.net.ipsec.ike.cts.PacketUtils.UDP_HDRLEN;
+import static android.net.ipsec.ike.cts.PacketUtils.UdpHeader;
+import static android.system.OsConstants.IPPROTO_UDP;
+
+import static com.android.internal.util.HexDump.hexStringToByteArray;
+
+import static org.junit.Assert.fail;
+
+import android.os.ParcelFileDescriptor;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Predicate;
+
+public class IkeTunUtils extends TunUtils {
+    private static final int PORT_LEN = 2;
+
+    private static final int NON_ESP_MARKER_LEN = 4;
+    private static final byte[] NON_ESP_MARKER = new byte[NON_ESP_MARKER_LEN];
+
+    private static final int IKE_INIT_SPI_OFFSET = 0;
+    private static final int IKE_FIRST_PAYLOAD_OFFSET = 16;
+    private static final int IKE_IS_RESP_BYTE_OFFSET = 19;
+    private static final int IKE_MSG_ID_OFFSET = 20;
+    private static final int IKE_HEADER_LEN = 28;
+    private static final int IKE_FRAG_NUM_OFFSET = 32;
+    private static final int IKE_PAYLOAD_TYPE_SKF = 53;
+
+    private static final int RSP_FLAG_MASK = 0x20;
+
+    public IkeTunUtils(ParcelFileDescriptor tunFd) {
+        super(tunFd);
+    }
+
+    /**
+     * Await the expected IKE request inject an IKE response (or a list of response fragments)
+     *
+     * @param ikeRespDataFragmentsHex IKE response hex (or a list of response fragments) without
+     *     IP/UDP headers or NON ESP MARKER.
+     */
+    public byte[] awaitReqAndInjectResp(
+            long expectedInitIkeSpi,
+            int expectedMsgId,
+            boolean expectedUseEncap,
+            String... ikeRespDataFragmentsHex)
+            throws Exception {
+        return awaitReqAndInjectResp(
+                        expectedInitIkeSpi,
+                        expectedMsgId,
+                        expectedUseEncap,
+                        1 /* expectedReqPktCnt */,
+                        ikeRespDataFragmentsHex)
+                .get(0);
+    }
+
+    /**
+     * Await the expected IKE request (or the list of IKE request fragments) and inject an IKE
+     * response (or a list of response fragments)
+     *
+     * @param ikeRespDataFragmentsHex IKE response hex (or a list of response fragments) without
+     *     IP/UDP headers or NON ESP MARKER.
+     */
+    public List<byte[]> awaitReqAndInjectResp(
+            long expectedInitIkeSpi,
+            int expectedMsgId,
+            boolean expectedUseEncap,
+            int expectedReqPktCnt,
+            String... ikeRespDataFragmentsHex)
+            throws Exception {
+        List<byte[]> reqList = new ArrayList<>(expectedReqPktCnt);
+        if (expectedReqPktCnt == 1) {
+            // Expecting one complete IKE packet
+            byte[] req =
+                    awaitIkePacket(
+                            (pkt) -> {
+                                return isExpectedIkePkt(
+                                        pkt,
+                                        expectedInitIkeSpi,
+                                        expectedMsgId,
+                                        false /* expectedResp */,
+                                        expectedUseEncap);
+                            });
+            reqList.add(req);
+        } else {
+            // Expecting "expectedReqPktCnt" number of request fragments
+            for (int i = 0; i < expectedReqPktCnt; i++) {
+                // IKE Fragment number always starts from 1
+                int expectedFragNum = i + 1;
+                byte[] req =
+                        awaitIkePacket(
+                                (pkt) -> {
+                                    return isExpectedIkeFragPkt(
+                                            pkt,
+                                            expectedInitIkeSpi,
+                                            expectedMsgId,
+                                            false /* expectedResp */,
+                                            expectedUseEncap,
+                                            expectedFragNum);
+                                });
+                reqList.add(req);
+            }
+        }
+
+        // All request fragments have the same addresses and ports
+        byte[] request = reqList.get(0);
+
+        // Build response header by flipping address and port
+        InetAddress srcAddr = getAddress(request, false /* shouldGetSource */);
+        InetAddress dstAddr = getAddress(request, true /* shouldGetSource */);
+        int srcPort = getPort(request, false /* shouldGetSource */);
+        int dstPort = getPort(request, true /* shouldGetSource */);
+        for (String resp : ikeRespDataFragmentsHex) {
+            byte[] response =
+                    buildIkePacket(
+                            srcAddr,
+                            dstAddr,
+                            srcPort,
+                            dstPort,
+                            expectedUseEncap,
+                            hexStringToByteArray(resp));
+            injectPacket(response);
+        }
+
+        return reqList;
+    }
+
+    /** Await the expected IKE response */
+    public byte[] awaitResp(long expectedInitIkeSpi, int expectedMsgId, boolean expectedUseEncap)
+            throws Exception {
+        return awaitIkePacket(
+                (pkt) -> {
+                    return isExpectedIkePkt(
+                            pkt,
+                            expectedInitIkeSpi,
+                            expectedMsgId,
+                            true /* expectedResp*/,
+                            expectedUseEncap);
+                });
+    }
+
+    private byte[] awaitIkePacket(Predicate<byte[]> pktVerifier) throws Exception {
+        long endTime = System.currentTimeMillis() + TIMEOUT;
+        int startIndex = 0;
+        synchronized (mPackets) {
+            while (System.currentTimeMillis() < endTime) {
+                byte[] ikePkt = getFirstMatchingPacket(pktVerifier, startIndex);
+                if (ikePkt != null) {
+                    return ikePkt; // We've found the packet we're looking for.
+                }
+
+                startIndex = mPackets.size();
+
+                // Try to prevent waiting too long. If waitTimeout <= 0, we've already hit timeout
+                long waitTimeout = endTime - System.currentTimeMillis();
+                if (waitTimeout > 0) {
+                    mPackets.wait(waitTimeout);
+                }
+            }
+
+            fail("No matching packet found");
+        }
+
+        throw new IllegalStateException(
+                "Hit an impossible case where fail() didn't throw an exception");
+    }
+
+    private static boolean isExpectedIkePkt(
+            byte[] pkt,
+            long expectedInitIkeSpi,
+            int expectedMsgId,
+            boolean expectedResp,
+            boolean expectedUseEncap) {
+        int ipProtocolOffset = isIpv6(pkt) ? IP6_PROTO_OFFSET : IP4_PROTO_OFFSET;
+        int ikeOffset = getIkeOffset(pkt, expectedUseEncap);
+
+        return pkt[ipProtocolOffset] == IPPROTO_UDP
+                && expectedUseEncap == hasNonEspMarker(pkt)
+                && isExpectedSpiAndMsgId(
+                        pkt, ikeOffset, expectedInitIkeSpi, expectedMsgId, expectedResp);
+    }
+
+    private static boolean isExpectedIkeFragPkt(
+            byte[] pkt,
+            long expectedInitIkeSpi,
+            int expectedMsgId,
+            boolean expectedResp,
+            boolean expectedUseEncap,
+            int expectedFragNum) {
+        return isExpectedIkePkt(
+                        pkt, expectedInitIkeSpi, expectedMsgId, expectedResp, expectedUseEncap)
+                && isExpectedFragNum(pkt, getIkeOffset(pkt, expectedUseEncap), expectedFragNum);
+    }
+
+    private static int getIkeOffset(byte[] pkt, boolean useEncap) {
+        if (isIpv6(pkt)) {
+            // IPv6 UDP expectedUseEncap not supported by kernels; assume non-expectedUseEncap.
+            return IP6_HDRLEN + UDP_HDRLEN;
+        } else {
+            // Use default IPv4 header length (assuming no options)
+            int ikeOffset = IP4_HDRLEN + UDP_HDRLEN;
+            return useEncap ? ikeOffset + NON_ESP_MARKER_LEN : ikeOffset;
+        }
+    }
+
+    private static boolean hasNonEspMarker(byte[] pkt) {
+        ByteBuffer buffer = ByteBuffer.wrap(pkt);
+        int ikeOffset = IP4_HDRLEN + UDP_HDRLEN;
+        if (buffer.remaining() < ikeOffset) return false;
+
+        buffer.get(new byte[ikeOffset]); // Skip IP and UDP header
+        byte[] nonEspMarker = new byte[NON_ESP_MARKER_LEN];
+        if (buffer.remaining() < NON_ESP_MARKER_LEN) return false;
+
+        buffer.get(nonEspMarker);
+        return Arrays.equals(NON_ESP_MARKER, nonEspMarker);
+    }
+
+    private static boolean isExpectedSpiAndMsgId(
+            byte[] pkt,
+            int ikeOffset,
+            long expectedInitIkeSpi,
+            int expectedMsgId,
+            boolean expectedResp) {
+        if (pkt.length <= ikeOffset + IKE_HEADER_LEN) return false;
+
+        ByteBuffer buffer = ByteBuffer.wrap(pkt);
+        buffer.get(new byte[ikeOffset]); // Skip IP, UDP header (and NON_ESP_MARKER)
+        buffer.mark(); // Mark this position so that later we can reset back here
+
+        // Check SPI
+        buffer.get(new byte[IKE_INIT_SPI_OFFSET]);
+        long initSpi = buffer.getLong();
+        if (expectedInitIkeSpi != initSpi) {
+            return false;
+        }
+
+        // Check direction
+        buffer.reset();
+        buffer.get(new byte[IKE_IS_RESP_BYTE_OFFSET]);
+        byte flagsByte = buffer.get();
+        boolean isResp = ((flagsByte & RSP_FLAG_MASK) != 0);
+        if (expectedResp != isResp) {
+            return false;
+        }
+
+        // Check message ID
+        buffer.reset();
+        buffer.get(new byte[IKE_MSG_ID_OFFSET]);
+
+        // Both the expected message ID and the packet's msgId are signed integers, so directly
+        // compare them.
+        int msgId = buffer.getInt();
+        if (expectedMsgId != msgId) {
+            return false;
+        }
+
+        return true;
+    }
+
+    private static boolean isExpectedFragNum(byte[] pkt, int ikeOffset, int expectedFragNum) {
+        ByteBuffer buffer = ByteBuffer.wrap(pkt);
+        buffer.get(new byte[ikeOffset]);
+        buffer.mark(); // Mark this position so that later we can reset back here
+
+        // Check if it is a fragment packet
+        buffer.get(new byte[IKE_FIRST_PAYLOAD_OFFSET]);
+        int firstPayload = Byte.toUnsignedInt(buffer.get());
+        if (firstPayload != IKE_PAYLOAD_TYPE_SKF) {
+            return false;
+        }
+
+        // Check fragment number
+        buffer.reset();
+        buffer.get(new byte[IKE_FRAG_NUM_OFFSET]);
+        int fragNum = Short.toUnsignedInt(buffer.getShort());
+        return expectedFragNum == fragNum;
+    }
+
+    public static class PortPair {
+        public final int srcPort;
+        public final int dstPort;
+
+        public PortPair(int sourcePort, int destinationPort) {
+            srcPort = sourcePort;
+            dstPort = destinationPort;
+        }
+    }
+
+    public static PortPair getSrcDestPortPair(byte[] outboundIkePkt) throws Exception {
+        return new PortPair(
+                getPort(outboundIkePkt, true /* shouldGetSource */),
+                getPort(outboundIkePkt, false /* shouldGetSource */));
+    }
+
+    private static InetAddress getAddress(byte[] pkt, boolean shouldGetSource) throws Exception {
+        int ipLen = isIpv6(pkt) ? IP6_ADDR_LEN : IP4_ADDR_LEN;
+        int srcIpOffset = isIpv6(pkt) ? IP6_ADDR_OFFSET : IP4_ADDR_OFFSET;
+        int ipOffset = shouldGetSource ? srcIpOffset : srcIpOffset + ipLen;
+
+        ByteBuffer buffer = ByteBuffer.wrap(pkt);
+        buffer.get(new byte[ipOffset]);
+        byte[] ipAddrBytes = new byte[ipLen];
+        buffer.get(ipAddrBytes);
+        return InetAddress.getByAddress(ipAddrBytes);
+    }
+
+    private static int getPort(byte[] pkt, boolean shouldGetSource) {
+        ByteBuffer buffer = ByteBuffer.wrap(pkt);
+        int srcPortOffset = isIpv6(pkt) ? IP6_HDRLEN : IP4_HDRLEN;
+        int portOffset = shouldGetSource ? srcPortOffset : srcPortOffset + PORT_LEN;
+
+        buffer.get(new byte[portOffset]);
+        return Short.toUnsignedInt(buffer.getShort());
+    }
+
+    public static byte[] buildIkePacket(
+            InetAddress srcAddr,
+            InetAddress dstAddr,
+            int srcPort,
+            int dstPort,
+            boolean useEncap,
+            byte[] ikePacket)
+            throws Exception {
+        if (useEncap) {
+            ByteBuffer buffer = ByteBuffer.allocate(NON_ESP_MARKER_LEN + ikePacket.length);
+            buffer.put(NON_ESP_MARKER);
+            buffer.put(ikePacket);
+            ikePacket = buffer.array();
+        }
+
+        UdpHeader udpPkt = new UdpHeader(srcPort, dstPort, new BytePayload(ikePacket));
+        IpHeader ipPkt = getIpHeader(udpPkt.getProtocolId(), srcAddr, dstAddr, udpPkt);
+        return ipPkt.getPacketBytes();
+    }
+
+    private static IpHeader getIpHeader(
+            int protocol, InetAddress src, InetAddress dst, Payload payload) {
+        if ((src instanceof Inet6Address) != (dst instanceof Inet6Address)) {
+            throw new IllegalArgumentException("Invalid src/dst address combination");
+        }
+
+        if (src instanceof Inet6Address) {
+            return new Ip6Header(protocol, (Inet6Address) src, (Inet6Address) dst, payload);
+        } else {
+            return new Ip4Header(protocol, (Inet4Address) src, (Inet4Address) dst, payload);
+        }
+    }
+}
diff --git a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/TunUtils.java b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/TunUtils.java
index 71450ea..5539dbc 100644
--- a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/TunUtils.java
+++ b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/TunUtils.java
@@ -47,18 +47,18 @@
     private static final String TAG = TunUtils.class.getSimpleName();
 
     private static final int DATA_BUFFER_LEN = 4096;
-    private static final int TIMEOUT = 100;
+    static final int TIMEOUT = 500;
 
-    private static final int IP4_PROTO_OFFSET = 9;
-    private static final int IP6_PROTO_OFFSET = 6;
+    static final int IP4_PROTO_OFFSET = 9;
+    static final int IP6_PROTO_OFFSET = 6;
 
-    private static final int IP4_ADDR_OFFSET = 12;
-    private static final int IP4_ADDR_LEN = 4;
-    private static final int IP6_ADDR_OFFSET = 8;
-    private static final int IP6_ADDR_LEN = 16;
+    static final int IP4_ADDR_OFFSET = 12;
+    static final int IP4_ADDR_LEN = 4;
+    static final int IP6_ADDR_OFFSET = 8;
+    static final int IP6_ADDR_LEN = 16;
 
+    final List<byte[]> mPackets = new ArrayList<>();
     private final ParcelFileDescriptor mTunFd;
-    private final List<byte[]> mPackets = new ArrayList<>();
     private final Thread mReaderThread;
 
     public TunUtils(ParcelFileDescriptor tunFd) {
@@ -107,7 +107,7 @@
         return Arrays.copyOf(inBytes, bytesRead);
     }
 
-    private byte[] getFirstMatchingPacket(Predicate<byte[]> verifier, int startIndex) {
+    byte[] getFirstMatchingPacket(Predicate<byte[]> verifier, int startIndex) {
         synchronized (mPackets) {
             for (int i = startIndex; i < mPackets.size(); i++) {
                 byte[] pkt = mPackets.get(i);
@@ -198,7 +198,7 @@
         }
     }
 
-    private static boolean isIpv6(byte[] pkt) {
+    static boolean isIpv6(byte[] pkt) {
         // First nibble shows IP version. 0x60 for IPv6
         return (pkt[0] & (byte) 0xF0) == (byte) 0x60;
     }
diff --git a/tests/cts/net/src/android/net/cts/CaptivePortalApiTest.kt b/tests/cts/net/src/android/net/cts/CaptivePortalApiTest.kt
new file mode 100644
index 0000000..40d0ca6
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/CaptivePortalApiTest.kt
@@ -0,0 +1,271 @@
+/*
+ * 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 android.net.cts
+
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.Manifest.permission.NETWORK_SETTINGS
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.EthernetManager
+import android.net.InetAddresses
+import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkRequest
+import android.net.TestNetworkInterface
+import android.net.TestNetworkManager
+import android.net.Uri
+import android.net.dhcp.DhcpDiscoverPacket
+import android.net.dhcp.DhcpPacket
+import android.net.dhcp.DhcpPacket.DHCP_MESSAGE_TYPE
+import android.net.dhcp.DhcpPacket.DHCP_MESSAGE_TYPE_DISCOVER
+import android.net.dhcp.DhcpPacket.DHCP_MESSAGE_TYPE_REQUEST
+import android.net.dhcp.DhcpRequestPacket
+import android.net.shared.Inet4AddressUtils.getBroadcastAddress
+import android.net.shared.Inet4AddressUtils.getPrefixMaskAsInet4Address
+import android.os.Build
+import android.os.HandlerThread
+import android.platform.test.annotations.AppModeFull
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.runner.AndroidJUnit4
+import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
+import com.android.compatibility.common.util.ThrowingRunnable
+import com.android.server.util.NetworkStackConstants.IPV4_ADDR_ANY
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DhcpClientPacketFilter
+import com.android.testutils.DhcpOptionFilter
+import com.android.testutils.RecorderCallback.CallbackEntry
+import com.android.testutils.TapPacketReader
+import com.android.testutils.TestableNetworkCallback
+import fi.iki.elonen.NanoHTTPD
+import org.junit.After
+import org.junit.Assume.assumeFalse
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.net.Inet4Address
+import java.util.concurrent.ArrayBlockingQueue
+import java.util.concurrent.TimeUnit
+import kotlin.reflect.KClass
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+private const val MAX_PACKET_LENGTH = 1500
+private const val TEST_TIMEOUT_MS = 10_000L
+
+private const val TEST_LEASE_TIMEOUT_SECS = 3600 * 12
+private const val TEST_PREFIX_LENGTH = 24
+
+private const val TEST_LOGIN_URL = "https://login.capport.android.com"
+private const val TEST_VENUE_INFO_URL = "https://venueinfo.capport.android.com"
+private const val TEST_DOMAIN_NAME = "lan"
+private const val TEST_MTU = 1500.toShort()
+
+@AppModeFull(reason = "Instant apps cannot create test networks")
+@RunWith(AndroidJUnit4::class)
+class CaptivePortalApiTest {
+    @JvmField
+    @Rule
+    val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = Build.VERSION_CODES.Q)
+
+    private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
+    private val tnm by lazy { context.assertHasService(TestNetworkManager::class.java) }
+    private val eth by lazy { context.assertHasService(EthernetManager::class.java) }
+    private val cm by lazy { context.assertHasService(ConnectivityManager::class.java) }
+
+    private val handlerThread = HandlerThread(CaptivePortalApiTest::class.simpleName)
+    private val serverIpAddr = InetAddresses.parseNumericAddress("192.0.2.222") as Inet4Address
+    private val clientIpAddr = InetAddresses.parseNumericAddress("192.0.2.111") as Inet4Address
+    private val httpServer = HttpServer()
+    private val ethRequest = NetworkRequest.Builder()
+            // ETHERNET|TEST transport networks do not have NET_CAPABILITY_TRUSTED
+            .removeCapability(NET_CAPABILITY_TRUSTED)
+            .addTransportType(TRANSPORT_ETHERNET).build()
+    private val ethRequestCb = TestableNetworkCallback()
+
+    private lateinit var iface: TestNetworkInterface
+    private lateinit var reader: TapPacketReader
+    private lateinit var capportUrl: Uri
+
+    private var testSkipped = false
+
+    @Before
+    fun setUp() {
+        // This test requires using a tap interface as the default ethernet interface: skip if there
+        // is already an ethernet interface connected.
+        testSkipped = eth.isAvailable()
+        assumeFalse(testSkipped)
+
+        // Register a request so the network does not get torn down
+        cm.requestNetwork(ethRequest, ethRequestCb)
+        runAsShell(NETWORK_SETTINGS, MANAGE_TEST_NETWORKS) {
+            eth.setIncludeTestInterfaces(true)
+            // Keeping a reference to the test interface also makes sure the ParcelFileDescriptor
+            // does not go out of scope, which would cause it to close the underlying FileDescriptor
+            // in its finalizer.
+            iface = tnm.createTapInterface()
+        }
+
+        handlerThread.start()
+        reader = TapPacketReader(
+                handlerThread.threadHandler,
+                iface.fileDescriptor.fileDescriptor,
+                MAX_PACKET_LENGTH)
+        handlerThread.threadHandler.post { reader.start() }
+        httpServer.start()
+
+        // Pad the listening port to make sure it is always of length 5. This ensures the URL has
+        // always the same length so the test can use constant IP and UDP header lengths.
+        // The maximum port number is 65535 so a length of 5 is always enough.
+        capportUrl = Uri.parse("http://localhost:${httpServer.listeningPort}/testapi.html?par=val")
+    }
+
+    @After
+    fun tearDown() {
+        if (testSkipped) return
+        cm.unregisterNetworkCallback(ethRequestCb)
+
+        runAsShell(NETWORK_SETTINGS) { eth.setIncludeTestInterfaces(false) }
+
+        httpServer.stop()
+        handlerThread.threadHandler.post { reader.stop() }
+        handlerThread.quitSafely()
+
+        iface.fileDescriptor.close()
+    }
+
+    @Test
+    fun testApiCallbacks() {
+        // Handle the DHCP handshake that includes the capport API URL
+        val discover = reader.assertDhcpPacketReceived(
+                DhcpDiscoverPacket::class, TEST_TIMEOUT_MS, DHCP_MESSAGE_TYPE_DISCOVER)
+        reader.sendResponse(makeOfferPacket(discover.clientMac, discover.transactionId))
+
+        val request = reader.assertDhcpPacketReceived(
+                DhcpRequestPacket::class, TEST_TIMEOUT_MS, DHCP_MESSAGE_TYPE_REQUEST)
+        assertEquals(discover.transactionId, request.transactionId)
+        assertEquals(clientIpAddr, request.mRequestedIp)
+        reader.sendResponse(makeAckPacket(request.clientMac, request.transactionId))
+
+        // Expect a request to the capport API
+        val capportReq = httpServer.recordedRequests.poll(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS)
+        assertNotNull(capportReq, "The device did not fetch captive portal API data within timeout")
+        assertEquals(capportUrl.path, capportReq.uri)
+        assertEquals(capportUrl.query, capportReq.queryParameterString)
+
+        // Expect network callbacks with capport info
+        val testCb = TestableNetworkCallback(TEST_TIMEOUT_MS)
+        // LinkProperties do not contain captive portal info if the callback is registered without
+        // NETWORK_SETTINGS permissions.
+        val lp = runAsShell(NETWORK_SETTINGS) {
+            cm.registerNetworkCallback(ethRequest, testCb)
+
+            try {
+                val ncCb = testCb.eventuallyExpect<CallbackEntry.CapabilitiesChanged> {
+                    it.caps.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL)
+                }
+                testCb.eventuallyExpect<CallbackEntry.LinkPropertiesChanged> {
+                    it.network == ncCb.network && it.lp.captivePortalData != null
+                }.lp
+            } finally {
+                cm.unregisterNetworkCallback(testCb)
+            }
+        }
+
+        assertEquals(capportUrl, lp.captivePortalApiUrl)
+        with(lp.captivePortalData) {
+            assertNotNull(this)
+            assertTrue(isCaptive)
+            assertEquals(Uri.parse(TEST_LOGIN_URL), userPortalUrl)
+            assertEquals(Uri.parse(TEST_VENUE_INFO_URL), venueInfoUrl)
+        }
+    }
+
+    private fun makeOfferPacket(clientMac: ByteArray, transactionId: Int) =
+            DhcpPacket.buildOfferPacket(DhcpPacket.ENCAP_L2, transactionId,
+                    false /* broadcast */, serverIpAddr, IPV4_ADDR_ANY /* relayIp */, clientIpAddr,
+                    clientMac, TEST_LEASE_TIMEOUT_SECS,
+                    getPrefixMaskAsInet4Address(TEST_PREFIX_LENGTH),
+                    getBroadcastAddress(clientIpAddr, TEST_PREFIX_LENGTH),
+                    listOf(serverIpAddr) /* gateways */, listOf(serverIpAddr) /* dnsServers */,
+                    serverIpAddr, TEST_DOMAIN_NAME, null /* hostname */, true /* metered */,
+                    TEST_MTU, capportUrl.toString())
+
+    private fun makeAckPacket(clientMac: ByteArray, transactionId: Int) =
+            DhcpPacket.buildAckPacket(DhcpPacket.ENCAP_L2, transactionId,
+                    false /* broadcast */, serverIpAddr, IPV4_ADDR_ANY /* relayIp */, clientIpAddr,
+                    clientIpAddr /* requestClientIp */, clientMac, TEST_LEASE_TIMEOUT_SECS,
+                    getPrefixMaskAsInet4Address(TEST_PREFIX_LENGTH),
+                    getBroadcastAddress(clientIpAddr, TEST_PREFIX_LENGTH),
+                    listOf(serverIpAddr) /* gateways */, listOf(serverIpAddr) /* dnsServers */,
+                    serverIpAddr, TEST_DOMAIN_NAME, null /* hostname */, true /* metered */,
+                    TEST_MTU, false /* rapidCommit */, capportUrl.toString())
+
+    private fun parseDhcpPacket(bytes: ByteArray) = DhcpPacket.decodeFullPacket(
+            bytes, MAX_PACKET_LENGTH, DhcpPacket.ENCAP_L2)
+}
+
+/**
+ * A minimal HTTP server running on localhost (loopback), on a random available port.
+ *
+ * The server records each request in [recordedRequests] and will not serve any further request
+ * until the last one is removed from the queue for verification.
+ */
+private class HttpServer : NanoHTTPD("localhost", 0 /* auto-select the port */) {
+    val recordedRequests = ArrayBlockingQueue<IHTTPSession>(1 /* capacity */)
+
+    override fun serve(session: IHTTPSession): Response {
+        recordedRequests.offer(session)
+        return newFixedLengthResponse("""
+                |{
+                |  "captive": true,
+                |  "user-portal-url": "$TEST_LOGIN_URL",
+                |  "venue-info-url": "$TEST_VENUE_INFO_URL"
+                |}
+            """.trimMargin())
+    }
+}
+
+private fun <T : DhcpPacket> TapPacketReader.assertDhcpPacketReceived(
+    packetType: KClass<T>,
+    timeoutMs: Long,
+    type: Byte
+): T {
+    val packetBytes = popPacket(timeoutMs, DhcpClientPacketFilter()
+            .and(DhcpOptionFilter(DHCP_MESSAGE_TYPE, type)))
+            ?: fail("${packetType.simpleName} not received within timeout")
+    val packet = DhcpPacket.decodeFullPacket(packetBytes, packetBytes.size, DhcpPacket.ENCAP_L2)
+    assertTrue(packetType.isInstance(packet),
+            "Expected ${packetType.simpleName} but got ${packet.javaClass.simpleName}")
+    return packetType.java.cast(packet)
+}
+
+private fun <T> Context.assertHasService(manager: Class<T>): T {
+    return getSystemService(manager) ?: fail("Service $manager not found")
+}
+
+/**
+ * Wrapper around runWithShellPermissionIdentity with kotlin-like syntax.
+ */
+private fun <T> runAsShell(vararg permissions: String, task: () -> T): T {
+    var ret: T? = null
+    runWithShellPermissionIdentity(ThrowingRunnable { ret = task() }, *permissions)
+    return ret ?: fail("ThrowingRunnable was not run")
+}
diff --git a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
new file mode 100644
index 0000000..0816aba
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
@@ -0,0 +1,259 @@
+/*
+ * 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 android.net.cts
+
+import android.Manifest.permission.CONNECTIVITY_INTERNAL
+import android.Manifest.permission.NETWORK_SETTINGS
+import android.Manifest.permission.READ_DEVICE_CONFIG
+import android.Manifest.permission.WRITE_DEVICE_CONFIG
+import android.content.pm.PackageManager.FEATURE_TELEPHONY
+import android.content.pm.PackageManager.FEATURE_WIFI
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.net.Uri
+import android.net.cts.util.CtsNetUtils
+import android.net.wifi.WifiManager
+import android.os.Build
+import android.os.ConditionVariable
+import android.platform.test.annotations.AppModeFull
+import android.provider.DeviceConfig
+import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
+import android.text.TextUtils
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import androidx.test.runner.AndroidJUnit4
+import com.android.compatibility.common.util.SystemUtil
+import fi.iki.elonen.NanoHTTPD
+import fi.iki.elonen.NanoHTTPD.Response.IStatus
+import fi.iki.elonen.NanoHTTPD.Response.Status
+import junit.framework.AssertionFailedError
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.runner.RunWith
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+import kotlin.test.Test
+import kotlin.test.assertNotEquals
+import kotlin.test.assertTrue
+
+private const val TEST_CAPTIVE_PORTAL_HTTPS_URL_SETTING = "test_captive_portal_https_url"
+private const val TEST_CAPTIVE_PORTAL_HTTP_URL_SETTING = "test_captive_portal_http_url"
+private const val TEST_URL_EXPIRATION_TIME = "test_url_expiration_time"
+
+private const val TEST_HTTPS_URL_PATH = "https_path"
+private const val TEST_HTTP_URL_PATH = "http_path"
+private const val TEST_PORTAL_URL_PATH = "portal_path"
+
+private const val LOCALHOST_HOSTNAME = "localhost"
+
+// Re-connecting to the AP, obtaining an IP address, revalidating can take a long time
+private const val WIFI_CONNECT_TIMEOUT_MS = 120_000L
+private const val TEST_TIMEOUT_MS = 10_000L
+
+private fun <T> CompletableFuture<T>.assertGet(timeoutMs: Long, message: String): T {
+    try {
+        return get(timeoutMs, TimeUnit.MILLISECONDS)
+    } catch (e: TimeoutException) {
+        throw AssertionFailedError(message)
+    }
+}
+
+@AppModeFull(reason = "WRITE_DEVICE_CONFIG permission can't be granted to instant apps")
+@RunWith(AndroidJUnit4::class)
+class CaptivePortalTest {
+    private val context: android.content.Context by lazy { getInstrumentation().context }
+    private val wm by lazy { context.getSystemService(WifiManager::class.java) }
+    private val cm by lazy { context.getSystemService(ConnectivityManager::class.java) }
+    private val pm by lazy { context.packageManager }
+    private val utils by lazy { CtsNetUtils(context) }
+
+    private val server = HttpServer()
+
+    @Before
+    fun setUp() {
+        doAsShell(READ_DEVICE_CONFIG) {
+            // Verify that the test URLs are not normally set on the device, but do not fail if the
+            // test URLs are set to what this test uses (URLs on localhost), in case the test was
+            // interrupted manually and rerun.
+            assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL_SETTING)
+            assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTP_URL_SETTING)
+        }
+        clearTestUrls()
+        server.start()
+    }
+
+    @After
+    fun tearDown() {
+        clearTestUrls()
+        server.stop()
+    }
+
+    private fun assertEmptyOrLocalhostUrl(urlKey: String) {
+        val url = DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY, urlKey)
+        assertTrue(TextUtils.isEmpty(url) || LOCALHOST_HOSTNAME == Uri.parse(url).host,
+                "$urlKey must not be set in production scenarios (current value: $url)")
+    }
+
+    private fun clearTestUrls() {
+        setHttpsUrl(null)
+        setHttpUrl(null)
+        setUrlExpiration(null)
+    }
+
+    @Test
+    fun testCaptivePortalIsNotDefaultNetwork() {
+        assumeTrue(pm.hasSystemFeature(FEATURE_TELEPHONY))
+        assumeTrue(pm.hasSystemFeature(FEATURE_WIFI))
+        utils.connectToWifi()
+        utils.connectToCell()
+
+        // Have network validation use a local server that serves a HTTPS error / HTTP redirect
+        server.addResponse(TEST_PORTAL_URL_PATH, Status.OK,
+                content = "Test captive portal content")
+        server.addResponse(TEST_HTTPS_URL_PATH, Status.INTERNAL_ERROR)
+        server.addResponse(TEST_HTTP_URL_PATH, Status.REDIRECT,
+                locationHeader = server.makeUrl(TEST_PORTAL_URL_PATH))
+        setHttpsUrl(server.makeUrl(TEST_HTTPS_URL_PATH))
+        setHttpUrl(server.makeUrl(TEST_HTTP_URL_PATH))
+        // URL expiration needs to be in the next 10 minutes
+        setUrlExpiration(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(9))
+
+        // Expect the portal content to be fetched at some point after detecting the portal.
+        // Some implementations may fetch the URL before startCaptivePortalApp is called.
+        val portalContentRequestCv = server.addExpectRequestCv(TEST_PORTAL_URL_PATH)
+
+        // Wait for a captive portal to be detected on the network
+        val wifiNetworkFuture = CompletableFuture<Network>()
+        val wifiCb = object : NetworkCallback() {
+            override fun onCapabilitiesChanged(
+                network: Network,
+                nc: NetworkCapabilities
+            ) {
+                if (nc.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL)) {
+                    wifiNetworkFuture.complete(network)
+                }
+            }
+        }
+        cm.requestNetwork(NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI).build(), wifiCb)
+
+        try {
+            reconnectWifi()
+            val network = wifiNetworkFuture.assertGet(WIFI_CONNECT_TIMEOUT_MS,
+                    "Captive portal not detected after ${WIFI_CONNECT_TIMEOUT_MS}ms")
+
+            val wifiDefaultMessage = "Wifi should not be the default network when a captive " +
+                    "portal was detected and another network (mobile data) can provide internet " +
+                    "access."
+            assertNotEquals(network, cm.activeNetwork, wifiDefaultMessage)
+
+            val startPortalAppPermission =
+                    if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) CONNECTIVITY_INTERNAL
+                    else NETWORK_SETTINGS
+            doAsShell(startPortalAppPermission) { cm.startCaptivePortalApp(network) }
+            assertTrue(portalContentRequestCv.block(TEST_TIMEOUT_MS), "The captive portal login " +
+                    "page was still not fetched ${TEST_TIMEOUT_MS}ms after startCaptivePortalApp.")
+
+            assertNotEquals(network, cm.activeNetwork, wifiDefaultMessage)
+        } finally {
+            cm.unregisterNetworkCallback(wifiCb)
+            server.stop()
+            // disconnectFromCell should be called after connectToCell
+            utils.disconnectFromCell()
+        }
+
+        clearTestUrls()
+        reconnectWifi()
+    }
+
+    private fun setHttpsUrl(url: String?) = setConfig(TEST_CAPTIVE_PORTAL_HTTPS_URL_SETTING, url)
+    private fun setHttpUrl(url: String?) = setConfig(TEST_CAPTIVE_PORTAL_HTTP_URL_SETTING, url)
+    private fun setUrlExpiration(timestamp: Long?) = setConfig(TEST_URL_EXPIRATION_TIME,
+            timestamp?.toString())
+
+    private fun setConfig(configKey: String, value: String?) {
+        doAsShell(WRITE_DEVICE_CONFIG) {
+            DeviceConfig.setProperty(
+                    NAMESPACE_CONNECTIVITY, configKey, value, false /* makeDefault */)
+        }
+    }
+
+    private fun doAsShell(vararg permissions: String, action: () -> Unit) {
+        // Wrap the below call to allow for more kotlin-like syntax
+        SystemUtil.runWithShellPermissionIdentity(action, permissions)
+    }
+
+    private fun reconnectWifi() {
+        doAsShell(NETWORK_SETTINGS) {
+            assertTrue(wm.disconnect())
+            assertTrue(wm.reconnect())
+        }
+    }
+
+    /**
+     * A minimal HTTP server running on localhost (loopback), on a random available port.
+     */
+    private class HttpServer : NanoHTTPD("localhost", 0 /* auto-select the port */) {
+        // Map of URL path -> HTTP response code
+        private val responses = HashMap<String, Response>()
+
+        // Map of path -> CV to open as soon as a request to the path is received
+        private val waitForRequestCv = HashMap<String, ConditionVariable>()
+
+        /**
+         * Create a URL string that, when fetched, will hit this server with the given URL [path].
+         */
+        fun makeUrl(path: String): String {
+            return Uri.Builder()
+                    .scheme("http")
+                    .encodedAuthority("localhost:$listeningPort")
+                    .query(path)
+                    .build()
+                    .toString()
+        }
+
+        fun addResponse(
+            path: String,
+            statusCode: IStatus,
+            locationHeader: String? = null,
+            content: String = ""
+        ) {
+            val response = newFixedLengthResponse(statusCode, "text/plain", content)
+            locationHeader?.let { response.addHeader("Location", it) }
+            responses[path] = response
+        }
+
+        /**
+         * Create a [ConditionVariable] that will open when a request to [path] is received.
+         */
+        fun addExpectRequestCv(path: String): ConditionVariable {
+            return ConditionVariable().apply { waitForRequestCv[path] = this }
+        }
+
+        override fun serve(session: IHTTPSession): Response {
+            waitForRequestCv[session.queryParameterString]?.open()
+            return responses[session.queryParameterString]
+                    // Default response is a 404
+                    ?: super.serve(session)
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 1ee08ff..d498ed9 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -16,13 +16,17 @@
 
 package android.net.cts;
 
+import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
 import static android.content.pm.PackageManager.FEATURE_ETHERNET;
 import static android.content.pm.PackageManager.FEATURE_TELEPHONY;
-import static android.content.pm.PackageManager.FEATURE_WIFI;
 import static android.content.pm.PackageManager.FEATURE_USB_HOST;
+import static android.content.pm.PackageManager.FEATURE_WIFI;
+import static android.content.pm.PackageManager.GET_PERMISSIONS;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_IMS;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.cts.util.CtsNetUtils.ConnectivityActionReceiver;
 import static android.net.cts.util.CtsNetUtils.HTTP_PORT;
@@ -36,6 +40,16 @@
 import static android.system.OsConstants.AF_UNSPEC;
 
 import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
 
 import android.annotation.NonNull;
 import android.app.Instrumentation;
@@ -45,6 +59,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.net.ConnectivityManager;
@@ -59,10 +74,12 @@
 import android.net.NetworkInfo.DetailedState;
 import android.net.NetworkInfo.State;
 import android.net.NetworkRequest;
+import android.net.NetworkUtils;
 import android.net.SocketKeepalive;
 import android.net.cts.util.CtsNetUtils;
 import android.net.util.KeepaliveUtils;
 import android.net.wifi.WifiManager;
+import android.os.Binder;
 import android.os.Build;
 import android.os.Looper;
 import android.os.MessageQueue;
@@ -71,15 +88,22 @@
 import android.os.VintfRuntimeInfo;
 import android.platform.test.annotations.AppModeFull;
 import android.provider.Settings;
-import android.test.AndroidTestCase;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.Pair;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.ArrayUtils;
 
 import libcore.io.Streams;
 
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.io.FileDescriptor;
 import java.io.IOException;
 import java.io.InputStream;
@@ -105,7 +129,8 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-public class ConnectivityManagerTest extends AndroidTestCase {
+@RunWith(AndroidJUnit4.class)
+public class ConnectivityManagerTest {
 
     private static final String TAG = ConnectivityManagerTest.class.getSimpleName();
 
@@ -117,7 +142,10 @@
     private static final int INTERVAL_KEEPALIVE_RETRY_MS = 500;
     private static final int MAX_KEEPALIVE_RETRY_COUNT = 3;
     private static final int MIN_KEEPALIVE_INTERVAL = 10;
-    private static final int NETWORK_CHANGE_METEREDNESS_TIMEOUT = 5000;
+
+    // Changing meteredness on wifi involves reconnecting, which can take several seconds (involves
+    // re-associating, DHCP...)
+    private static final int NETWORK_CHANGE_METEREDNESS_TIMEOUT = 30_000;
     private static final int NUM_TRIES_MULTIPATH_PREF_CHECK = 20;
     private static final long INTERVAL_MULTIPATH_PREF_CHECK_MS = 500;
     // device could have only one interface: data, wifi.
@@ -141,22 +169,19 @@
     private PackageManager mPackageManager;
     private final HashMap<Integer, NetworkConfig> mNetworks =
             new HashMap<Integer, NetworkConfig>();
-    boolean mWifiConnectAttempted;
+    boolean mWifiWasDisabled;
     private UiAutomation mUiAutomation;
     private CtsNetUtils mCtsNetUtils;
-    private boolean mShellPermissionIdentityAdopted;
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        Looper.prepare();
-        mContext = getContext();
+    @Before
+    public void setUp() throws Exception {
         mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        mContext = mInstrumentation.getContext();
         mCm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
         mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
         mPackageManager = mContext.getPackageManager();
         mCtsNetUtils = new CtsNetUtils(mContext);
-        mWifiConnectAttempted = false;
+        mWifiWasDisabled = false;
 
         // Get com.android.internal.R.array.networkAttributes
         int resId = mContext.getResources().getIdentifier("networkAttributes", "array", "android");
@@ -173,20 +198,17 @@
             } catch (Exception e) {}
         }
         mUiAutomation = mInstrumentation.getUiAutomation();
-        mShellPermissionIdentityAdopted = false;
     }
 
-    @Override
-    protected void tearDown() throws Exception {
+    @After
+    public void tearDown() throws Exception {
         // Return WiFi to its original disabled state after tests that explicitly connect.
-        if (mWifiConnectAttempted) {
+        if (mWifiWasDisabled) {
             mCtsNetUtils.disconnectFromWifi(null);
         }
         if (mCtsNetUtils.cellConnectAttempted()) {
             mCtsNetUtils.disconnectFromCell();
         }
-        dropShellPermissionIdentity();
-        super.tearDown();
     }
 
     /**
@@ -195,13 +217,12 @@
      * automatically in tearDown().
      */
     private Network ensureWifiConnected() {
-        if (mWifiManager.isWifiEnabled()) {
-            return mCtsNetUtils.getWifiNetwork();
-        }
-        mWifiConnectAttempted = true;
+        mWifiWasDisabled = !mWifiManager.isWifiEnabled();
+        // Even if wifi is enabled, the network may not be connected or ready yet
         return mCtsNetUtils.connectToWifi();
     }
 
+    @Test
     public void testIsNetworkTypeValid() {
         assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE));
         assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_WIFI));
@@ -231,12 +252,14 @@
 
     }
 
+    @Test
     public void testSetNetworkPreference() {
         // getNetworkPreference() and setNetworkPreference() are both deprecated so they do
         // not preform any action.  Verify they are at least still callable.
         mCm.setNetworkPreference(mCm.getNetworkPreference());
     }
 
+    @Test
     public void testGetActiveNetworkInfo() {
         NetworkInfo ni = mCm.getActiveNetworkInfo();
 
@@ -245,6 +268,7 @@
         assertTrue(ni.getState() == State.CONNECTED);
     }
 
+    @Test
     public void testGetActiveNetwork() {
         Network network = mCm.getActiveNetwork();
         assertNotNull("You must have an active network connection to complete CTS", network);
@@ -257,6 +281,7 @@
         assertTrue(ni.getState() == State.CONNECTED);
     }
 
+    @Test
     public void testGetNetworkInfo() {
         for (int type = -1; type <= ConnectivityManager.MAX_NETWORK_TYPE+1; type++) {
             if (shouldBeSupported(type)) {
@@ -275,6 +300,7 @@
         }
     }
 
+    @Test
     public void testGetAllNetworkInfo() {
         NetworkInfo[] ni = mCm.getAllNetworkInfo();
         assertTrue(ni.length >= MIN_NUM_NETWORK_TYPES);
@@ -298,6 +324,7 @@
      * and that they are made from different IP addresses.
      */
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+    @Test
     public void testOpenConnection() throws Exception {
         boolean canRunTest = mPackageManager.hasSystemFeature(FEATURE_WIFI)
                 && mPackageManager.hasSystemFeature(FEATURE_TELEPHONY);
@@ -377,6 +404,7 @@
         } catch (UnsupportedOperationException expected) {}
     }
 
+    @Test
     public void testStartUsingNetworkFeature() {
 
         final String invalidateFeature = "invalidateFeature";
@@ -406,6 +434,7 @@
                (networkType == ConnectivityManager.TYPE_ETHERNET && shouldEthernetBeSupported());
     }
 
+    @Test
     public void testIsNetworkSupported() {
         for (int type = -1; type <= ConnectivityManager.MAX_NETWORK_TYPE; type++) {
             boolean supported = mCm.isNetworkSupported(type);
@@ -417,12 +446,14 @@
         }
     }
 
+    @Test
     public void testRequestRouteToHost() {
         for (int type = -1 ; type <= ConnectivityManager.MAX_NETWORK_TYPE; type++) {
             assertRequestRouteToHostUnsupported(type, HOST_ADDRESS);
         }
     }
 
+    @Test
     public void testTest() {
         mCm.getBackgroundDataSetting();
     }
@@ -443,6 +474,7 @@
      * that it would increase test coverage by much (how many devices have 3G radio but not Wifi?).
      */
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+    @Test
     public void testRegisterNetworkCallback() {
         if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
             Log.i(TAG, "testRegisterNetworkCallback cannot execute unless device supports WiFi");
@@ -484,6 +516,7 @@
      * of a {@code NetworkCallback}.
      */
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+    @Test
     public void testRegisterNetworkCallback_withPendingIntent() {
         if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
             Log.i(TAG, "testRegisterNetworkCallback cannot execute unless device supports WiFi");
@@ -529,6 +562,7 @@
      * see if we get a callback for an INTERNET request.
      */
     @AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
+    @Test
     public void testRequestNetworkCallback() {
         final TestNetworkCallback callback = new TestNetworkCallback();
         mCm.requestNetwork(new NetworkRequest.Builder()
@@ -552,6 +586,7 @@
      * fail. Use WIFI and switch Wi-Fi off.
      */
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+    @Test
     public void testRequestNetworkCallback_onUnavailable() {
         final boolean previousWifiEnabledState = mWifiManager.isWifiEnabled();
         if (previousWifiEnabledState) {
@@ -589,6 +624,7 @@
 
     /** Verify restricted networks cannot be requested. */
     @AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
+    @Test
     public void testRestrictedNetworks() {
         // Verify we can request unrestricted networks:
         NetworkRequest request = new NetworkRequest.Builder()
@@ -710,6 +746,7 @@
      * for metered and unmetered networks.
      */
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+    @Test
     public void testGetMultipathPreference() throws Exception {
         final ContentResolver resolver = mContext.getContentResolver();
         ensureWifiConnected();
@@ -878,18 +915,6 @@
                 keepalivesPerTransport, nc);
     }
 
-    private void adoptShellPermissionIdentity() {
-        mUiAutomation.adoptShellPermissionIdentity();
-        mShellPermissionIdentityAdopted = true;
-    }
-
-    private void dropShellPermissionIdentity() {
-        if (mShellPermissionIdentityAdopted) {
-            mUiAutomation.dropShellPermissionIdentity();
-            mShellPermissionIdentityAdopted = false;
-        }
-    }
-
     private static boolean isTcpKeepaliveSupportedByKernel() {
         final String kVersionString = VintfRuntimeInfo.getKernelVersion();
         return compareMajorMinorVersion(kVersionString, "4.8") >= 0;
@@ -924,6 +949,7 @@
      * Verifies that version string compare logic returns expected result for various cases.
      * Note that only major and minor number are compared.
      */
+    @Test
     public void testMajorMinorVersionCompare() {
         assertEquals(0, compareMajorMinorVersion("4.8.1", "4.8"));
         assertEquals(1, compareMajorMinorVersion("4.9", "4.8.1"));
@@ -943,6 +969,7 @@
      * keepalives is set to 0.
      */
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+    @Test
     public void testKeepaliveWifiUnsupported() throws Exception {
         if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
             Log.i(TAG, "testKeepaliveUnsupported cannot execute unless device"
@@ -952,32 +979,36 @@
 
         final Network network = ensureWifiConnected();
         if (getSupportedKeepalivesForNet(network) != 0) return;
+        final InetAddress srcAddr = getFirstV4Address(network);
+        assumeTrue("This test requires native IPv4", srcAddr != null);
 
-        adoptShellPermissionIdentity();
-
-        assertEquals(0, createConcurrentSocketKeepalives(network, 1, 0));
-        assertEquals(0, createConcurrentSocketKeepalives(network, 0, 1));
-
-        dropShellPermissionIdentity();
+        runWithShellPermissionIdentity(() -> {
+            assertEquals(0, createConcurrentSocketKeepalives(network, srcAddr, 1, 0));
+            assertEquals(0, createConcurrentSocketKeepalives(network, srcAddr, 0, 1));
+        });
     }
 
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+    @Test
     public void testCreateTcpKeepalive() throws Exception {
         if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
             Log.i(TAG, "testCreateTcpKeepalive cannot execute unless device supports WiFi");
             return;
         }
 
-        adoptShellPermissionIdentity();
-
         final Network network = ensureWifiConnected();
         if (getSupportedKeepalivesForNet(network) == 0) return;
+        final InetAddress srcAddr = getFirstV4Address(network);
+        assumeTrue("This test requires native IPv4", srcAddr != null);
+
         // If kernel < 4.8 then it doesn't support TCP keepalive, but it might still support
         // NAT-T keepalive. If keepalive limits from resource overlay is not zero, TCP keepalive
         // needs to be supported except if the kernel doesn't support it.
         if (!isTcpKeepaliveSupportedByKernel()) {
             // Sanity check to ensure the callback result is expected.
-            assertEquals(0, createConcurrentSocketKeepalives(network, 0, 1));
+            runWithShellPermissionIdentity(() -> {
+                assertEquals(0, createConcurrentSocketKeepalives(network, srcAddr, 0, 1));
+            });
             Log.i(TAG, "testCreateTcpKeepalive is skipped for kernel "
                     + VintfRuntimeInfo.getKernelVersion());
             return;
@@ -991,6 +1022,8 @@
             // Should able to start keep alive offload when socket is idle.
             final Executor executor = mContext.getMainExecutor();
             final TestSocketKeepaliveCallback callback = new TestSocketKeepaliveCallback();
+
+            mUiAutomation.adoptShellPermissionIdentity();
             try (SocketKeepalive sk = mCm.createSocketKeepalive(network, s, executor, callback)) {
                 sk.start(MIN_KEEPALIVE_INTERVAL);
                 callback.expectStarted();
@@ -1012,6 +1045,8 @@
                 // Stop.
                 sk.stop();
                 callback.expectStopped();
+            } finally {
+                mUiAutomation.dropShellPermissionIdentity();
             }
 
             // Ensure socket is still connected.
@@ -1040,9 +1075,12 @@
 
             // Should get ERROR_SOCKET_NOT_IDLE because there is still data in the receive queue
             // that has not been read.
+            mUiAutomation.adoptShellPermissionIdentity();
             try (SocketKeepalive sk = mCm.createSocketKeepalive(network, s, executor, callback)) {
                 sk.start(MIN_KEEPALIVE_INTERVAL);
                 callback.expectError(SocketKeepalive.ERROR_SOCKET_NOT_IDLE);
+            } finally {
+                mUiAutomation.dropShellPermissionIdentity();
             }
         }
     }
@@ -1087,7 +1125,7 @@
     }
 
     private @NonNull ArrayList<SocketKeepalive> createConcurrentNattSocketKeepalives(
-            @NonNull Network network, int requestCount,
+            @NonNull Network network, @NonNull InetAddress srcAddr, int requestCount,
             @NonNull TestSocketKeepaliveCallback callback)  throws Exception {
 
         final Executor executor = mContext.getMainExecutor();
@@ -1095,7 +1133,6 @@
         // Initialize a real NaT-T socket.
         final IpSecManager mIpSec = (IpSecManager) mContext.getSystemService(Context.IPSEC_SERVICE);
         final UdpEncapsulationSocket nattSocket = mIpSec.openUdpEncapsulationSocket();
-        final InetAddress srcAddr = getFirstV4Address(network);
         final InetAddress dstAddr = getAddrByName(TEST_HOST, AF_INET);
         assertNotNull(srcAddr);
         assertNotNull(dstAddr);
@@ -1136,11 +1173,12 @@
      * @return the total number of keepalives created.
      */
     private int createConcurrentSocketKeepalives(
-            @NonNull Network network, int nattCount, int tcpCount) throws Exception {
+            @NonNull Network network, @NonNull InetAddress srcAddr, int nattCount, int tcpCount)
+            throws Exception {
         final ArrayList<SocketKeepalive> kalist = new ArrayList<>();
         final TestSocketKeepaliveCallback callback = new TestSocketKeepaliveCallback();
 
-        kalist.addAll(createConcurrentNattSocketKeepalives(network, nattCount, callback));
+        kalist.addAll(createConcurrentNattSocketKeepalives(network, srcAddr, nattCount, callback));
         kalist.addAll(createConcurrentTcpSocketKeepalives(network, tcpCount, callback));
 
         final int ret = kalist.size();
@@ -1160,6 +1198,7 @@
      * get leaked after iterations.
      */
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+    @Test
     public void testSocketKeepaliveLimitWifi() throws Exception {
         if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
             Log.i(TAG, "testSocketKeepaliveLimitWifi cannot execute unless device"
@@ -1172,33 +1211,39 @@
         if (supported == 0) {
             return;
         }
+        final InetAddress srcAddr = getFirstV4Address(network);
+        assumeTrue("This test requires native IPv4", srcAddr != null);
 
-        adoptShellPermissionIdentity();
+        runWithShellPermissionIdentity(() -> {
+            // Verifies that the supported keepalive slots meet MIN_SUPPORTED_KEEPALIVE_COUNT.
+            assertGreaterOrEqual(supported, MIN_SUPPORTED_WIFI_KEEPALIVE_COUNT);
 
-        // Verifies that the supported keepalive slots meet MIN_SUPPORTED_KEEPALIVE_COUNT.
-        assertGreaterOrEqual(supported, MIN_SUPPORTED_WIFI_KEEPALIVE_COUNT);
-
-        // Verifies that Nat-T keepalives can be established.
-        assertEquals(supported, createConcurrentSocketKeepalives(network, supported + 1, 0));
-        // Verifies that keepalives don't get leaked in second round.
-        assertEquals(supported, createConcurrentSocketKeepalives(network, supported, 0));
+            // Verifies that Nat-T keepalives can be established.
+            assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr,
+                    supported + 1, 0));
+            // Verifies that keepalives don't get leaked in second round.
+            assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr, supported,
+                    0));
+        });
 
         // If kernel < 4.8 then it doesn't support TCP keepalive, but it might still support
         // NAT-T keepalive. Test below cases only if TCP keepalive is supported by kernel.
-        if (isTcpKeepaliveSupportedByKernel()) {
-            assertEquals(supported, createConcurrentSocketKeepalives(network, 0, supported + 1));
+        if (!isTcpKeepaliveSupportedByKernel()) return;
+
+        runWithShellPermissionIdentity(() -> {
+            assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr, 0,
+                    supported + 1));
 
             // Verifies that different types can be established at the same time.
-            assertEquals(supported, createConcurrentSocketKeepalives(network,
+            assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr,
                     supported / 2, supported - supported / 2));
 
             // Verifies that keepalives don't get leaked in second round.
-            assertEquals(supported, createConcurrentSocketKeepalives(network, 0, supported));
-            assertEquals(supported, createConcurrentSocketKeepalives(network,
+            assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr, 0,
+                    supported));
+            assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr,
                     supported / 2, supported - supported / 2));
-        }
-
-        dropShellPermissionIdentity();
+        });
     }
 
     /**
@@ -1206,6 +1251,7 @@
      * don't get leaked after iterations.
      */
     @AppModeFull(reason = "Cannot request network in instant app mode")
+    @Test
     public void testSocketKeepaliveLimitTelephony() throws Exception {
         if (!mPackageManager.hasSystemFeature(FEATURE_TELEPHONY)) {
             Log.i(TAG, "testSocketKeepaliveLimitTelephony cannot execute unless device"
@@ -1222,18 +1268,19 @@
 
         final Network network = mCtsNetUtils.connectToCell();
         final int supported = getSupportedKeepalivesForNet(network);
+        final InetAddress srcAddr = getFirstV4Address(network);
+        assumeTrue("This test requires native IPv4", srcAddr != null);
 
-        adoptShellPermissionIdentity();
-
-        // Verifies that the supported keepalive slots meet minimum requirement.
-        assertGreaterOrEqual(supported, MIN_SUPPORTED_CELLULAR_KEEPALIVE_COUNT);
-
-        // Verifies that Nat-T keepalives can be established.
-        assertEquals(supported, createConcurrentSocketKeepalives(network, supported + 1, 0));
-        // Verifies that keepalives don't get leaked in second round.
-        assertEquals(supported, createConcurrentSocketKeepalives(network, supported, 0));
-
-        dropShellPermissionIdentity();
+        runWithShellPermissionIdentity(() -> {
+            // Verifies that the supported keepalive slots meet minimum requirement.
+            assertGreaterOrEqual(supported, MIN_SUPPORTED_CELLULAR_KEEPALIVE_COUNT);
+            // Verifies that Nat-T keepalives can be established.
+            assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr,
+                    supported + 1, 0));
+            // Verifies that keepalives don't get leaked in second round.
+            assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr, supported,
+                    0));
+        });
     }
 
     private int getIntResourceForName(@NonNull String resName) {
@@ -1246,6 +1293,7 @@
      * Verifies that the keepalive slots are limited as customized for unprivileged requests.
      */
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+    @Test
     public void testSocketKeepaliveUnprivileged() throws Exception {
         if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
             Log.i(TAG, "testSocketKeepaliveUnprivileged cannot execute unless device"
@@ -1258,6 +1306,8 @@
         if (supported == 0) {
             return;
         }
+        final InetAddress srcAddr = getFirstV4Address(network);
+        assumeTrue("This test requires native IPv4", srcAddr != null);
 
         // Resource ID might be shifted on devices that compiled with different symbols.
         // Thus, resolve ID at runtime is needed.
@@ -1273,11 +1323,46 @@
         final int expectedUnprivileged =
                 Math.min(allowedUnprivilegedPerUid, supported - reservedPrivilegedSlots);
         assertEquals(expectedUnprivileged,
-                createConcurrentSocketKeepalives(network, supported + 1, 0));
+                createConcurrentSocketKeepalives(network, srcAddr, supported + 1, 0));
     }
 
     private static void assertGreaterOrEqual(long greater, long lesser) {
         assertTrue("" + greater + " expected to be greater than or equal to " + lesser,
                 greater >= lesser);
     }
+
+    /**
+     * Verifies that apps are not allowed to access restricted networks even if they declare the
+     * CONNECTIVITY_USE_RESTRICTED_NETWORKS permission in their manifests.
+     * See. b/144679405.
+     */
+    @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+    @Test
+    public void testRestrictedNetworkPermission() throws Exception {
+        // Ensure that CONNECTIVITY_USE_RESTRICTED_NETWORKS isn't granted to this package.
+        final PackageInfo app = mPackageManager.getPackageInfo(mContext.getPackageName(),
+                GET_PERMISSIONS);
+        final int index = ArrayUtils.indexOf(
+                app.requestedPermissions, CONNECTIVITY_USE_RESTRICTED_NETWORKS);
+        assertTrue(index >= 0);
+        assertTrue(app.requestedPermissionsFlags[index] != PERMISSION_GRANTED);
+
+        // Ensure that NetworkUtils.queryUserAccess always returns false since this package should
+        // not have netd system permission to call this function.
+        final Network wifiNetwork = ensureWifiConnected();
+        assertFalse(NetworkUtils.queryUserAccess(Binder.getCallingUid(), wifiNetwork.netId));
+
+        // Ensure that this package cannot bind to any restricted network that's currently
+        // connected.
+        Network[] networks = mCm.getAllNetworks();
+        for (Network network : networks) {
+            NetworkCapabilities nc = mCm.getNetworkCapabilities(network);
+            if (nc != null && !nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) {
+                try {
+                    network.bindSocket(new Socket());
+                    fail("Bind to restricted network " + network + " unexpectedly succeeded");
+                } catch (IOException expected) {}
+            }
+        }
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/DnsResolverTest.java b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
index 1cc49f9..28753ff 100644
--- a/tests/cts/net/src/android/net/cts/DnsResolverTest.java
+++ b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
@@ -86,7 +86,6 @@
     static final int CANCEL_RETRY_TIMES = 5;
     static final int QUERY_TIMES = 10;
     static final int NXDOMAIN = 3;
-    static final int PRIVATE_DNS_SETTING_TIMEOUT_MS = 6_000;
 
     private ContentResolver mCR;
     private ConnectivityManager mCM;
@@ -107,32 +106,15 @@
         mExecutorInline = (Runnable r) -> r.run();
         mCR = getContext().getContentResolver();
         mCtsNetUtils = new CtsNetUtils(getContext());
-        storePrivateDnsSetting();
+        mCtsNetUtils.storePrivateDnsSetting();
     }
 
     @Override
     protected void tearDown() throws Exception {
-        restorePrivateDnsSetting();
+        mCtsNetUtils.restorePrivateDnsSetting();
         super.tearDown();
     }
 
-    private void storePrivateDnsSetting() {
-        // Store private DNS setting
-        mOldMode = Settings.Global.getString(mCR, Settings.Global.PRIVATE_DNS_MODE);
-        mOldDnsSpecifier = Settings.Global.getString(mCR, Settings.Global.PRIVATE_DNS_SPECIFIER);
-    }
-
-    private void restorePrivateDnsSetting() throws InterruptedException {
-        // restore private DNS setting
-        Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE, mOldMode);
-        if ("hostname".equals(mOldMode)) {
-            Settings.Global.putString(
-                mCR, Settings.Global.PRIVATE_DNS_SPECIFIER, mOldDnsSpecifier);
-            mCtsNetUtils.awaitPrivateDnsSetting("restorePrivateDnsSetting timeout",
-                    mCM.getActiveNetwork(), mOldDnsSpecifier, PRIVATE_DNS_SETTING_TIMEOUT_MS, true);
-        }
-    }
-
     private static String byteArrayToHexString(byte[] bytes) {
         char[] hexChars = new char[bytes.length * 2];
         for (int i = 0; i < bytes.length; ++i) {
@@ -416,16 +398,13 @@
         final String msg = "RawQuery " + TEST_NX_DOMAIN + " with private DNS";
         // Enable private DNS strict mode and set server to dns.google before doing NxDomain test.
         // b/144521720
-        Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE, "hostname");
-        Settings.Global.putString(mCR,
-                Settings.Global.PRIVATE_DNS_SPECIFIER, GOOGLE_PRIVATE_DNS_SERVER);
+        mCtsNetUtils.setPrivateDnsStrictMode(GOOGLE_PRIVATE_DNS_SERVER);
         for (Network network :  getTestableNetworks()) {
             final Network networkForPrivateDns =
                     (network != null) ? network : mCM.getActiveNetwork();
             assertNotNull("Can't find network to await private DNS on", networkForPrivateDns);
             mCtsNetUtils.awaitPrivateDnsSetting(msg + " wait private DNS setting timeout",
-                    networkForPrivateDns, GOOGLE_PRIVATE_DNS_SERVER,
-                    PRIVATE_DNS_SETTING_TIMEOUT_MS, true);
+                    networkForPrivateDns, GOOGLE_PRIVATE_DNS_SERVER, true);
             final VerifyCancelCallback callback = new VerifyCancelCallback(msg);
             mDns.rawQuery(network, TEST_NX_DOMAIN, CLASS_IN, TYPE_AAAA, FLAG_NO_CACHE_LOOKUP,
                     executor, null, callback);
@@ -688,9 +667,7 @@
         final Network[] testNetworks = getTestableNetworks();
 
         // Set an invalid private DNS server
-        Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE, "hostname");
-        Settings.Global.putString(mCR,
-                Settings.Global.PRIVATE_DNS_SPECIFIER, INVALID_PRIVATE_DNS_SERVER);
+        mCtsNetUtils.setPrivateDnsStrictMode(INVALID_PRIVATE_DNS_SERVER);
         final String msg = "Test PrivateDnsBypass " + TEST_DOMAIN;
         for (Network network : testNetworks) {
             // This test cannot be ran with null network because we need to explicitly pass a
@@ -699,7 +676,7 @@
 
             // wait for private DNS setting propagating
             mCtsNetUtils.awaitPrivateDnsSetting(msg + " wait private DNS setting timeout",
-                    network, INVALID_PRIVATE_DNS_SERVER, PRIVATE_DNS_SETTING_TIMEOUT_MS, false);
+                    network, INVALID_PRIVATE_DNS_SERVER, false);
 
             final CountDownLatch latch = new CountDownLatch(1);
             final DnsResolver.Callback<List<InetAddress>> errorCallback =
diff --git a/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
index f123187..985e313 100644
--- a/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
+++ b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
@@ -41,7 +41,6 @@
 
     private static final String TAG = "MultinetworkNativeApiTest";
     static final String GOOGLE_PRIVATE_DNS_SERVER = "dns.google";
-    static final int PRIVATE_DNS_SETTING_TIMEOUT_MS = 2_000;
 
     /**
      * @return 0 on success
@@ -69,7 +68,7 @@
         mCM = (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
         mCR = getContext().getContentResolver();
         mCtsNetUtils = new CtsNetUtils(getContext());
-        storePrivateDnsSetting();
+        mCtsNetUtils.storePrivateDnsSetting();
     }
 
     @Override
@@ -77,18 +76,6 @@
         super.tearDown();
     }
 
-    private void storePrivateDnsSetting() {
-        // Store private DNS setting
-        mOldMode = Settings.Global.getString(mCR, Settings.Global.PRIVATE_DNS_MODE);
-        mOldDnsSpecifier = Settings.Global.getString(mCR, Settings.Global.PRIVATE_DNS_SPECIFIER);
-    }
-
-    private void restorePrivateDnsSetting() {
-        // restore private DNS setting
-        Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE, mOldMode);
-        Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_SPECIFIER, mOldDnsSpecifier);
-    }
-
     private Network[] getTestableNetworks() {
         final ArrayList<Network> testableNetworks = new ArrayList<Network>();
         for (Network network : mCM.getAllNetworks()) {
@@ -239,17 +226,15 @@
         // Enable private DNS strict mode and set server to dns.google before doing NxDomain test.
         // b/144521720
         try {
-            Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE, "hostname");
-            Settings.Global.putString(mCR,
-                    Settings.Global.PRIVATE_DNS_SPECIFIER, GOOGLE_PRIVATE_DNS_SERVER);
+            mCtsNetUtils.setPrivateDnsStrictMode(GOOGLE_PRIVATE_DNS_SERVER);
             for (Network network : getTestableNetworks()) {
               // Wait for private DNS setting to propagate.
               mCtsNetUtils.awaitPrivateDnsSetting("NxDomain test wait private DNS setting timeout",
-                        network, GOOGLE_PRIVATE_DNS_SERVER, PRIVATE_DNS_SETTING_TIMEOUT_MS, true);
+                        network, GOOGLE_PRIVATE_DNS_SERVER, true);
               runResNnxDomainCheck(network.getNetworkHandle());
             }
         } finally {
-            restorePrivateDnsSetting();
+            mCtsNetUtils.restorePrivateDnsSetting();
         }
     }
 }
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index 03b961b..2824db7 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -592,4 +592,50 @@
             assertNull(it.uri)
         }
     }
+
+    @Test
+    fun testTemporarilyUnmeteredCapability() {
+        // This test will create a networks with/without NET_CAPABILITY_TEMPORARILY_NOT_METERED
+        // and check that the callback reflects the capability changes.
+        // First create a request to make sure the network is kept up
+        val request1 = NetworkRequest.Builder()
+                .clearCapabilities()
+                .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+                .build()
+        val callback1 = TestableNetworkCallback(DEFAULT_TIMEOUT_MS).also {
+            registerNetworkCallback(request1, it)
+        }
+        requestNetwork(request1, callback1)
+
+        // Then file the interesting request
+        val request = NetworkRequest.Builder()
+                .clearCapabilities()
+                .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+                .build()
+        val callback = TestableNetworkCallback()
+        requestNetwork(request, callback)
+
+        // Connect the network
+        createConnectedNetworkAgent().let { (agent, _) ->
+            callback.expectAvailableThenValidatedCallbacks(agent.network)
+
+            // Send TEMP_NOT_METERED and check that the callback is called appropriately.
+            val nc1 = NetworkCapabilities(agent.nc)
+                    .addCapability(NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+            agent.sendNetworkCapabilities(nc1)
+            callback.expectCapabilitiesThat(agent.network) {
+                it.hasCapability(NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+            }
+
+            // Remove TEMP_NOT_METERED and check that the callback is called appropriately.
+            val nc2 = NetworkCapabilities(agent.nc)
+                    .removeCapability(NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+            agent.sendNetworkCapabilities(nc2)
+            callback.expectCapabilitiesThat(agent.network) {
+                !it.hasCapability(NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+            }
+        }
+
+        // tearDown() will unregister the requests and agents
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
index 5e92b41..d118c8a 100644
--- a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
@@ -18,6 +18,7 @@
 
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED;
 import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
@@ -33,11 +34,12 @@
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
 import android.net.NetworkSpecifier;
-import android.net.TelephonyNetworkSpecifier;
+import android.net.UidRange;
 import android.net.wifi.WifiNetworkSpecifier;
 import android.os.Build;
-import android.os.Process;
 import android.os.PatternMatcher;
+import android.os.Process;
+import android.util.ArraySet;
 
 import androidx.test.runner.AndroidJUnit4;
 
@@ -59,6 +61,20 @@
     private static final String TEST_PACKAGE_NAME = "test.package.name";
     private static final MacAddress ARBITRARY_ADDRESS = MacAddress.fromString("3:5:8:12:9:2");
 
+    private class LocalNetworkSpecifier extends NetworkSpecifier {
+        private final int mId;
+
+        LocalNetworkSpecifier(int id) {
+            mId = id;
+        }
+
+        @Override
+        public boolean canBeSatisfiedBy(NetworkSpecifier other) {
+            return other instanceof LocalNetworkSpecifier
+                && mId == ((LocalNetworkSpecifier) other).mId;
+        }
+    }
+
     @Test
     public void testCapabilities() {
         assertTrue(new NetworkRequest.Builder().addCapability(NET_CAPABILITY_MMS).build()
@@ -71,6 +87,16 @@
         verifyNoCapabilities(nr);
     }
 
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testTemporarilyNotMeteredCapability() {
+        assertTrue(new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED).build()
+                .hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED));
+        assertFalse(new NetworkRequest.Builder()
+                .removeCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED).build()
+                .hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED));
+    }
+
     private void verifyNoCapabilities(NetworkRequest nr) {
         // NetworkCapabilities.mNetworkCapabilities is defined as type long
         final int MAX_POSSIBLE_CAPABILITY = Long.SIZE;
@@ -129,54 +155,108 @@
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.Q)
     public void testCanBeSatisfiedBy() {
-        final TelephonyNetworkSpecifier specifier1 = new TelephonyNetworkSpecifier.Builder()
-                .setSubscriptionId(1234 /* subId */)
-                .build();
-        final TelephonyNetworkSpecifier specifier2 = new TelephonyNetworkSpecifier.Builder()
-                .setSubscriptionId(5678 /* subId */)
-                .build();
-        final NetworkCapabilities cap = new NetworkCapabilities()
+        final LocalNetworkSpecifier specifier1 = new LocalNetworkSpecifier(1234 /* id */);
+        final LocalNetworkSpecifier specifier2 = new LocalNetworkSpecifier(5678 /* id */);
+
+        final NetworkCapabilities capCellularMmsInternet = new NetworkCapabilities()
                 .addTransportType(TRANSPORT_CELLULAR)
                 .addCapability(NET_CAPABILITY_MMS)
                 .addCapability(NET_CAPABILITY_INTERNET);
-        final NetworkCapabilities capDualTransport = new NetworkCapabilities(cap)
-                .addTransportType(TRANSPORT_VPN);
-        final NetworkCapabilities capWithSpecifier1 =
-                new NetworkCapabilities(cap).setNetworkSpecifier(specifier1);
-        final NetworkCapabilities capDiffTransportWithSpecifier1 = new NetworkCapabilities()
+        final NetworkCapabilities capCellularVpnMmsInternet =
+                new NetworkCapabilities(capCellularMmsInternet).addTransportType(TRANSPORT_VPN);
+        final NetworkCapabilities capCellularMmsInternetSpecifier1 =
+                new NetworkCapabilities(capCellularMmsInternet).setNetworkSpecifier(specifier1);
+        final NetworkCapabilities capVpnInternetSpecifier1 = new NetworkCapabilities()
                 .addCapability(NET_CAPABILITY_INTERNET)
                 .addTransportType(TRANSPORT_VPN)
                 .setNetworkSpecifier(specifier1);
+        final NetworkCapabilities capCellularMmsInternetMatchallspecifier =
+                new NetworkCapabilities(capCellularMmsInternet)
+                    .setNetworkSpecifier(new MatchAllNetworkSpecifier());
+        final NetworkCapabilities capCellularMmsInternetSpecifier2 =
+                new NetworkCapabilities(capCellularMmsInternet).setNetworkSpecifier(specifier2);
 
-        final NetworkRequest requestWithSpecifier1 = new NetworkRequest.Builder()
+        final NetworkRequest requestCellularInternetSpecifier1 = new NetworkRequest.Builder()
                 .addTransportType(TRANSPORT_CELLULAR)
                 .addCapability(NET_CAPABILITY_INTERNET)
                 .setNetworkSpecifier(specifier1)
                 .build();
-        assertFalse(requestWithSpecifier1.canBeSatisfiedBy(null));
-        assertFalse(requestWithSpecifier1.canBeSatisfiedBy(new NetworkCapabilities()));
-        assertTrue(requestWithSpecifier1.canBeSatisfiedBy(new NetworkCapabilities(cap)
-                .setNetworkSpecifier(new MatchAllNetworkSpecifier())));
-        assertTrue(requestWithSpecifier1.canBeSatisfiedBy(cap));
-        assertTrue(requestWithSpecifier1.canBeSatisfiedBy(capWithSpecifier1));
-        assertTrue(requestWithSpecifier1.canBeSatisfiedBy(capDualTransport));
-        assertFalse(requestWithSpecifier1.canBeSatisfiedBy(
-                new NetworkCapabilities(cap).setNetworkSpecifier(specifier2)));
+        assertFalse(requestCellularInternetSpecifier1.canBeSatisfiedBy(null));
+        assertFalse(requestCellularInternetSpecifier1.canBeSatisfiedBy(new NetworkCapabilities()));
+        assertTrue(requestCellularInternetSpecifier1.canBeSatisfiedBy(
+                capCellularMmsInternetMatchallspecifier));
+        assertFalse(requestCellularInternetSpecifier1.canBeSatisfiedBy(capCellularMmsInternet));
+        assertTrue(requestCellularInternetSpecifier1.canBeSatisfiedBy(
+                capCellularMmsInternetSpecifier1));
+        assertFalse(requestCellularInternetSpecifier1.canBeSatisfiedBy(capCellularVpnMmsInternet));
+        assertFalse(requestCellularInternetSpecifier1.canBeSatisfiedBy(
+                capCellularMmsInternetSpecifier2));
 
-        final NetworkRequest request = new NetworkRequest.Builder()
+        final NetworkRequest requestCellularInternet = new NetworkRequest.Builder()
                 .addTransportType(TRANSPORT_CELLULAR)
                 .addCapability(NET_CAPABILITY_INTERNET)
                 .build();
-        assertTrue(request.canBeSatisfiedBy(cap));
-        assertTrue(request.canBeSatisfiedBy(capWithSpecifier1));
-        assertTrue(request.canBeSatisfiedBy(
-                new NetworkCapabilities(cap).setNetworkSpecifier(specifier2)));
-        assertFalse(request.canBeSatisfiedBy(capDiffTransportWithSpecifier1));
-        assertTrue(request.canBeSatisfiedBy(capDualTransport));
+        assertTrue(requestCellularInternet.canBeSatisfiedBy(capCellularMmsInternet));
+        assertTrue(requestCellularInternet.canBeSatisfiedBy(capCellularMmsInternetSpecifier1));
+        assertTrue(requestCellularInternet.canBeSatisfiedBy(capCellularMmsInternetSpecifier2));
+        assertFalse(requestCellularInternet.canBeSatisfiedBy(capVpnInternetSpecifier1));
+        assertTrue(requestCellularInternet.canBeSatisfiedBy(capCellularVpnMmsInternet));
+    }
 
-        assertEquals(requestWithSpecifier1.canBeSatisfiedBy(capWithSpecifier1),
-                new NetworkCapabilities(capWithSpecifier1)
-                    .satisfiedByNetworkCapabilities(capWithSpecifier1));
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testInvariantInCanBeSatisfiedBy() {
+        // Test invariant that result of NetworkRequest.canBeSatisfiedBy() should be the same with
+        // NetworkCapabilities.satisfiedByNetworkCapabilities().
+        final LocalNetworkSpecifier specifier1 = new LocalNetworkSpecifier(1234 /* id */);
+        final int uid = Process.myUid();
+        final ArraySet<UidRange> ranges = new ArraySet<>();
+        ranges.add(new UidRange(uid, uid));
+        final NetworkRequest requestCombination = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .setLinkUpstreamBandwidthKbps(1000)
+                .setNetworkSpecifier(specifier1)
+                .setSignalStrength(-123)
+                .setUids(ranges).build();
+        final NetworkCapabilities capCell = new NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_CELLULAR).build();
+        assertCorrectlySatisfies(false, requestCombination, capCell);
+
+        final NetworkCapabilities capCellInternet = new NetworkCapabilities.Builder(capCell)
+                .addCapability(NET_CAPABILITY_INTERNET).build();
+        assertCorrectlySatisfies(false, requestCombination, capCellInternet);
+
+        final NetworkCapabilities capCellInternetBW =
+                new NetworkCapabilities.Builder(capCellInternet)
+                    .setLinkUpstreamBandwidthKbps(1024).build();
+        assertCorrectlySatisfies(false, requestCombination, capCellInternetBW);
+
+        final NetworkCapabilities capCellInternetBWSpecifier1 =
+                new NetworkCapabilities.Builder(capCellInternetBW)
+                    .setNetworkSpecifier(specifier1).build();
+        assertCorrectlySatisfies(false, requestCombination, capCellInternetBWSpecifier1);
+
+        final NetworkCapabilities capCellInternetBWSpecifier1Signal =
+                new NetworkCapabilities.Builder(capCellInternetBWSpecifier1)
+                    .setSignalStrength(-123).build();
+        assertCorrectlySatisfies(true, requestCombination,
+                capCellInternetBWSpecifier1Signal);
+
+        final NetworkCapabilities capCellInternetBWSpecifier1SignalUid =
+                new NetworkCapabilities.Builder(capCellInternetBWSpecifier1Signal)
+                    .setOwnerUid(uid)
+                    .setAdministratorUids(new int [] {uid}).build();
+        assertCorrectlySatisfies(true, requestCombination,
+                capCellInternetBWSpecifier1SignalUid);
+    }
+
+    private void assertCorrectlySatisfies(boolean expect, NetworkRequest request,
+            NetworkCapabilities nc) {
+        assertEquals(expect, request.canBeSatisfiedBy(nc));
+        assertEquals(
+                request.canBeSatisfiedBy(nc),
+                request.networkCapabilities.satisfiedByNetworkCapabilities(nc));
     }
 
     @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
diff --git a/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java b/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java
new file mode 100644
index 0000000..1a48983
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java
@@ -0,0 +1,146 @@
+/*
+ * 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 android.net.cts;
+
+import static android.os.Process.INVALID_UID;
+
+import static org.junit.Assert.assertEquals;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.INetworkStatsService;
+import android.net.TrafficStats;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.test.AndroidTestCase;
+import android.util.SparseArray;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.CollectionUtils;
+import com.android.testutils.DevSdkIgnoreRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+@RunWith(AndroidJUnit4.class)
+public class NetworkStatsBinderTest {
+    // NOTE: These are shamelessly copied from TrafficStats.
+    private static final int TYPE_RX_BYTES = 0;
+    private static final int TYPE_RX_PACKETS = 1;
+    private static final int TYPE_TX_BYTES = 2;
+    private static final int TYPE_TX_PACKETS = 3;
+
+    @Rule
+    public DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule(
+            Build.VERSION_CODES.Q /* ignoreClassUpTo */);
+
+    private final SparseArray<Function<Integer, Long>> mUidStatsQueryOpArray = new SparseArray<>();
+
+    @Before
+    public void setUp() throws Exception {
+        mUidStatsQueryOpArray.put(TYPE_RX_BYTES, uid -> TrafficStats.getUidRxBytes(uid));
+        mUidStatsQueryOpArray.put(TYPE_RX_PACKETS, uid -> TrafficStats.getUidRxPackets(uid));
+        mUidStatsQueryOpArray.put(TYPE_TX_BYTES, uid -> TrafficStats.getUidTxBytes(uid));
+        mUidStatsQueryOpArray.put(TYPE_TX_PACKETS, uid -> TrafficStats.getUidTxPackets(uid));
+    }
+
+    private long getUidStatsFromBinder(int uid, int type) throws Exception {
+        Method getServiceMethod = Class.forName("android.os.ServiceManager")
+                .getDeclaredMethod("getService", new Class[]{String.class});
+        IBinder binder = (IBinder) getServiceMethod.invoke(null, Context.NETWORK_STATS_SERVICE);
+        INetworkStatsService nss = INetworkStatsService.Stub.asInterface(binder);
+        return nss.getUidStats(uid, type);
+    }
+
+    private int getFirstAppUidThat(@NonNull Predicate<Integer> predicate) {
+        PackageManager pm = InstrumentationRegistry.getContext().getPackageManager();
+        List<PackageInfo> apps = pm.getInstalledPackages(0 /* flags */);
+        final PackageInfo match = CollectionUtils.find(apps,
+                it -> it.applicationInfo != null && predicate.test(it.applicationInfo.uid));
+        if (match != null) return match.applicationInfo.uid;
+        return INVALID_UID;
+    }
+
+    @Test
+    public void testAccessUidStatsFromBinder() throws Exception {
+        final int myUid = Process.myUid();
+        final List<Integer> testUidList = new ArrayList<>();
+
+        // Prepare uid list for testing.
+        testUidList.add(INVALID_UID);
+        testUidList.add(Process.ROOT_UID);
+        testUidList.add(Process.SYSTEM_UID);
+        testUidList.add(myUid);
+        testUidList.add(Process.LAST_APPLICATION_UID);
+        testUidList.add(Process.LAST_APPLICATION_UID + 1);
+        // If available, pick another existing uid for testing that is not already contained
+        // in the list above.
+        final int notMyUid = getFirstAppUidThat(uid -> uid >= 0 && !testUidList.contains(uid));
+        if (notMyUid != INVALID_UID) testUidList.add(notMyUid);
+
+        for (final int uid : testUidList) {
+            for (int i = 0; i < mUidStatsQueryOpArray.size(); i++) {
+                final int type = mUidStatsQueryOpArray.keyAt(i);
+                try {
+                    final long uidStatsFromBinder = getUidStatsFromBinder(uid, type);
+                    final long uidTrafficStats = mUidStatsQueryOpArray.get(type).apply(uid);
+
+                    // Verify that UNSUPPORTED is returned if the uid is not current app uid.
+                    if (uid != myUid) {
+                        assertEquals(uidStatsFromBinder, TrafficStats.UNSUPPORTED);
+                    }
+                    // Verify that returned result is the same with the result get from
+                    // TrafficStats.
+                    // TODO: If the test is flaky then it should instead assert that the values
+                    //  are approximately similar.
+                    assertEquals("uidStats is not matched for query type " + type
+                                    + ", uid=" + uid + ", myUid=" + myUid, uidTrafficStats,
+                            uidStatsFromBinder);
+                } catch (IllegalAccessException e) {
+                    /* Java language access prevents exploitation. */
+                    return;
+                } catch (InvocationTargetException e) {
+                    /* Underlying method has been changed. */
+                    return;
+                } catch (ClassNotFoundException e) {
+                    /* not vulnerable if hidden API no longer available */
+                    return;
+                } catch (NoSuchMethodException e) {
+                    /* not vulnerable if hidden API no longer available */
+                    return;
+                } catch (RemoteException e) {
+                    return;
+                }
+            }
+        }
+    }
+}
diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
index 6214f89..f39b184 100644
--- a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
+++ b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
@@ -16,6 +16,8 @@
 
 package android.net.cts.util;
 
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 
@@ -27,6 +29,7 @@
 import android.annotation.NonNull;
 import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.ContentResolver;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.net.ConnectivityManager;
@@ -38,6 +41,7 @@
 import android.net.NetworkInfo.State;
 import android.net.NetworkRequest;
 import android.net.wifi.WifiManager;
+import android.provider.Settings;
 import android.system.Os;
 import android.system.OsConstants;
 import android.util.Log;
@@ -58,6 +62,7 @@
     private static final int SOCKET_TIMEOUT_MS = 2000;
     private static final int PRIVATE_DNS_PROBE_MS = 1_000;
 
+    public static final int PRIVATE_DNS_SETTING_TIMEOUT_MS = 6_000;
     public static final int HTTP_PORT = 80;
     public static final String TEST_HOST = "connectivitycheck.gstatic.com";
     public static final String HTTP_REQUEST =
@@ -68,15 +73,19 @@
     public static final String NETWORK_CALLBACK_ACTION =
             "ConnectivityManagerTest.NetworkCallbackAction";
 
-    private Context mContext;
-    private ConnectivityManager mCm;
-    private WifiManager mWifiManager;
+    private final Context mContext;
+    private final ConnectivityManager mCm;
+    private final ContentResolver mCR;
+    private final WifiManager mWifiManager;
     private TestNetworkCallback mCellNetworkCallback;
+    private String mOldPrivateDnsMode;
+    private String mOldPrivateDnsSpecifier;
 
     public CtsNetUtils(Context context) {
         mContext = context;
         mCm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
         mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+        mCR = context.getContentResolver();
     }
 
     // Toggle WiFi twice, leaving it in the state it started in
@@ -107,6 +116,8 @@
         boolean connected = false;
         try {
             SystemUtil.runShellCommand("svc wifi enable");
+            SystemUtil.runWithShellPermissionIdentity(() -> mWifiManager.reconnect(),
+                    NETWORK_SETTINGS);
             // Ensure we get both an onAvailable callback and a CONNECTIVITY_ACTION.
             wifiNetwork = callback.waitForAvailable();
             assertNotNull(wifiNetwork);
@@ -246,9 +257,51 @@
         return s;
     }
 
+    public void storePrivateDnsSetting() {
+        // Store private DNS setting
+        mOldPrivateDnsMode = Settings.Global.getString(mCR, Settings.Global.PRIVATE_DNS_MODE);
+        mOldPrivateDnsSpecifier = Settings.Global.getString(mCR,
+                Settings.Global.PRIVATE_DNS_SPECIFIER);
+        // It's possible that there is no private DNS default value in Settings.
+        // Give it a proper default mode which is opportunistic mode.
+        if (mOldPrivateDnsMode == null) {
+            mOldPrivateDnsSpecifier = "";
+            mOldPrivateDnsMode = PRIVATE_DNS_MODE_OPPORTUNISTIC;
+            Settings.Global.putString(mCR,
+                    Settings.Global.PRIVATE_DNS_SPECIFIER, mOldPrivateDnsSpecifier);
+            Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE, mOldPrivateDnsMode);
+        }
+    }
+
+    public void restorePrivateDnsSetting() throws InterruptedException {
+        if (mOldPrivateDnsMode == null || mOldPrivateDnsSpecifier == null) {
+            return;
+        }
+        // restore private DNS setting
+        if ("hostname".equals(mOldPrivateDnsMode)) {
+            setPrivateDnsStrictMode(mOldPrivateDnsSpecifier);
+            awaitPrivateDnsSetting("restorePrivateDnsSetting timeout",
+                    mCm.getActiveNetwork(),
+                    mOldPrivateDnsSpecifier, true);
+        } else {
+            Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE, mOldPrivateDnsMode);
+        }
+    }
+
+    public void setPrivateDnsStrictMode(String server) {
+        // To reduce flake rate, set PRIVATE_DNS_SPECIFIER before PRIVATE_DNS_MODE. This ensures
+        // that if the previous private DNS mode was not "hostname", the system only sees one
+        // EVENT_PRIVATE_DNS_SETTINGS_CHANGED event instead of two.
+        Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_SPECIFIER, server);
+        final String mode = Settings.Global.getString(mCR, Settings.Global.PRIVATE_DNS_MODE);
+        // If current private DNS mode is "hostname", we only need to set PRIVATE_DNS_SPECIFIER.
+        if (!"hostname".equals(mode)) {
+            Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE, "hostname");
+        }
+    }
+
     public void awaitPrivateDnsSetting(@NonNull String msg, @NonNull Network network,
-            @NonNull String server, int timeoutMs,
-            boolean requiresValidatedServers) throws InterruptedException {
+            @NonNull String server, boolean requiresValidatedServers) throws InterruptedException {
         CountDownLatch latch = new CountDownLatch(1);
         NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build();
         NetworkCallback callback = new NetworkCallback() {
@@ -263,7 +316,7 @@
             }
         };
         mCm.registerNetworkCallback(request, callback);
-        assertTrue(msg, latch.await(timeoutMs, TimeUnit.MILLISECONDS));
+        assertTrue(msg, latch.await(PRIVATE_DNS_SETTING_TIMEOUT_MS, TimeUnit.MILLISECONDS));
         mCm.unregisterNetworkCallback(callback);
         // Wait some time for NetworkMonitor's private DNS probe to complete. If we do not do
         // this, then the test could complete before the NetworkMonitor private DNS probe
diff --git a/tests/cts/tethering/AndroidTest.xml b/tests/cts/tethering/AndroidTest.xml
index 25051ba..e752e3a 100644
--- a/tests/cts/tethering/AndroidTest.xml
+++ b/tests/cts/tethering/AndroidTest.xml
@@ -16,6 +16,7 @@
 <configuration description="Config for CTS Tethering test cases">
     <option name="test-suite-tag" value="cts" />
     <option name="config-descriptor:metadata" key="component" value="networking" />
+    <option name="config-descriptor:metadata" key="token" value="SIM_CARD" />
     <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
     <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
     <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
@@ -27,4 +28,8 @@
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="android.tethering.cts" />
     </test>
+
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="mainline-module-package-name" value="com.google.android.tethering" />
+    </object>
 </configuration>
diff --git a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
index bbb9403..1055531 100644
--- a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
+++ b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
@@ -26,7 +26,6 @@
 import static android.net.TetheringManager.TETHER_ERROR_ENTITLEMENT_UNKNOWN;
 import static android.net.TetheringManager.TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION;
 import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
-import static android.net.TetheringManager.TETHER_ERROR_TETHER_IFACE_ERROR;
 import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_FAILED;
 import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STARTED;
 import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STOPPED;
@@ -57,9 +56,13 @@
 import android.net.TetheringManager.TetheringInterfaceRegexps;
 import android.net.TetheringManager.TetheringRequest;
 import android.net.cts.util.CtsNetUtils;
+import android.net.cts.util.CtsNetUtils.TestNetworkCallback;
 import android.net.wifi.WifiManager;
 import android.os.Bundle;
+import android.os.PersistableBundle;
 import android.os.ResultReceiver;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 
 import androidx.annotation.NonNull;
@@ -676,6 +679,26 @@
             mTM.requestLatestTetheringEntitlementResult(
                     TETHERING_WIFI, false, c -> c.run(), null);
         } catch (IllegalArgumentException expect) { }
+
+        // Override carrier config to ignore entitlement check.
+        final PersistableBundle bundle = new PersistableBundle();
+        bundle.putBoolean(CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL, false);
+        overrideCarrierConfig(bundle);
+
+        // Verify that requestLatestTetheringEntitlementResult() can get entitlement
+        // result TETHER_ERROR_NO_ERROR due to provisioning bypassed.
+        assertEntitlementResult(listener -> mTM.requestLatestTetheringEntitlementResult(
+                TETHERING_WIFI, false, c -> c.run(), listener), TETHER_ERROR_NO_ERROR);
+
+        // Reset carrier config.
+        overrideCarrierConfig(null);
+    }
+
+    private void overrideCarrierConfig(PersistableBundle bundle) {
+        final CarrierConfigManager configManager = (CarrierConfigManager) mContext
+                .getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        final int subId = SubscriptionManager.getDefaultSubscriptionId();
+        configManager.overrideConfig(subId, bundle);
     }
 
     @Test
@@ -692,7 +715,15 @@
                 mCtsNetUtils.disconnectFromWifi(null);
             }
 
-            final Network activeNetwork = mCm.getActiveNetwork();
+            final TestNetworkCallback networkCallback = new TestNetworkCallback();
+            Network activeNetwork = null;
+            try {
+                mCm.registerDefaultNetworkCallback(networkCallback);
+                activeNetwork = networkCallback.waitForAvailable();
+            } finally {
+                mCm.unregisterNetworkCallback(networkCallback);
+            }
+
             assertNotNull("No active network. Please ensure the device has working mobile data.",
                     activeNetwork);
             final NetworkCapabilities activeNetCap = mCm.getNetworkCapabilities(activeNetwork);