Merge "Initial CL for testing IkeSession creation"
diff --git a/tests/cts/net/ipsec/Android.bp b/tests/cts/net/ipsec/Android.bp
index f1f120b..16bdb05 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/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..ed67dd1
--- /dev/null
+++ b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionPskTest.java
@@ -0,0 +1,258 @@
+/*
+ * 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.IkeSessionConfiguration.EXTENSION_TYPE_FRAGMENTATION;
+import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_NO_PROPOSAL_CHOSEN;
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+
+import static com.android.internal.util.HexDump.hexStringToByteArray;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.net.ipsec.ike.ChildSessionConfiguration;
+import android.net.ipsec.ike.IkeFqdnIdentification;
+import android.net.ipsec.ike.IkeSession;
+import android.net.ipsec.ike.IkeSessionConfiguration;
+import android.net.ipsec.ike.IkeSessionConnectionInfo;
+import android.net.ipsec.ike.IkeSessionParams;
+import android.net.ipsec.ike.TunnelModeChildSessionParams;
+import android.net.ipsec.ike.exceptions.IkeException;
+import android.net.ipsec.ike.exceptions.IkeProtocolException;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetAddress;
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "MANAGE_TEST_NETWORKS 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 static final long IKE_INIT_SPI = Long.parseLong("46B8ECA1E0D72A18", 16);
+
+    private static final TunnelModeChildSessionParams CHILD_PARAMS =
+            new TunnelModeChildSessionParams.Builder()
+                    .addSaProposal(SaProposalTest.buildChildSaProposalWithNormalModeCipher())
+                    .addSaProposal(SaProposalTest.buildChildSaProposalWithCombinedModeCipher())
+                    .addInternalAddressRequest(AF_INET)
+                    .addInternalAddressRequest(AF_INET6)
+                    .build();
+
+    private IkeSessionParams createIkeSessionParams(InetAddress mRemoteAddress) {
+        return new IkeSessionParams.Builder(sContext)
+                .setNetwork(mTunNetwork)
+                .setServerHostname(mRemoteAddress.getHostAddress())
+                .addSaProposal(SaProposalTest.buildIkeSaProposalWithNormalModeCipher())
+                .addSaProposal(SaProposalTest.buildIkeSaProposalWithCombinedModeCipher())
+                .setLocalIdentification(new IkeFqdnIdentification(LOCAL_HOSTNAME))
+                .setRemoteIdentification(new IkeFqdnIdentification(REMOTE_HOSTNAME))
+                .setAuthPsk(IKE_PSK)
+                .build();
+    }
+
+    private IkeSession openIkeSession(IkeSessionParams ikeParams) {
+        return new IkeSession(
+                sContext,
+                ikeParams,
+                CHILD_PARAMS,
+                mUserCbExecutor,
+                mIkeSessionCallback,
+                mFirstChildSessionCallback);
+    }
+
+    @Test
+    public void testIkeSessionSetupAndManageChildSas() throws Exception {
+        // Open IKE Session
+        IkeSession ikeSession = openIkeSession(createIkeSessionParams(mRemoteAddress));
+        int expectedMsgId = 0;
+        mTunUtils.awaitReqAndInjectResp(
+                IKE_INIT_SPI,
+                expectedMsgId++,
+                false /* expectedUseEncap */,
+                hexStringToByteArray(SUCCESS_IKE_INIT_RESP));
+
+        mTunUtils.awaitReqAndInjectResp(
+                IKE_INIT_SPI,
+                expectedMsgId++,
+                true /* expectedUseEncap */,
+                hexStringToByteArray(SUCCESS_IKE_AUTH_RESP));
+
+        // Verify opening IKE Session
+        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());
+
+        // Verify opening first Child Session
+        ChildSessionConfiguration firstChildConfig = mFirstChildSessionCallback.awaitChildConfig();
+        assertNotNull(firstChildConfig);
+        assertEquals(
+                Arrays.asList(EXPECTED_INBOUND_TS), firstChildConfig.getInboundTrafficSelectors());
+        assertEquals(Arrays.asList(DEFAULT_V4_TS), firstChildConfig.getOutboundTrafficSelectors());
+        assertEquals(
+                Arrays.asList(EXPECTED_INTERNAL_LINK_ADDR),
+                firstChildConfig.getInternalAddresses());
+        assertTrue(firstChildConfig.getInternalSubnets().isEmpty());
+        assertTrue(firstChildConfig.getInternalDnsServers().isEmpty());
+        assertTrue(firstChildConfig.getInternalDhcpServers().isEmpty());
+
+        // Open additional Child Session
+        TestChildSessionCallback additionalChildCb = new TestChildSessionCallback();
+        ikeSession.openChildSession(CHILD_PARAMS, additionalChildCb);
+        mTunUtils.awaitReqAndInjectResp(
+                IKE_INIT_SPI,
+                expectedMsgId++,
+                true /* expectedUseEncap */,
+                hexStringToByteArray(SUCCESS_CREATE_CHILD_RESP));
+
+        // Verify opening additional Child Session
+        ChildSessionConfiguration additionalChildConfig = additionalChildCb.awaitChildConfig();
+        assertNotNull(additionalChildConfig);
+        assertEquals(
+                Arrays.asList(EXPECTED_INBOUND_TS), firstChildConfig.getInboundTrafficSelectors());
+        assertEquals(Arrays.asList(DEFAULT_V4_TS), firstChildConfig.getOutboundTrafficSelectors());
+        assertTrue(additionalChildConfig.getInternalAddresses().isEmpty());
+        assertTrue(firstChildConfig.getInternalSubnets().isEmpty());
+        assertTrue(firstChildConfig.getInternalDnsServers().isEmpty());
+        assertTrue(firstChildConfig.getInternalDhcpServers().isEmpty());
+
+        // Close additional Child Session
+        ikeSession.closeChildSession(additionalChildCb);
+        mTunUtils.awaitReqAndInjectResp(
+                IKE_INIT_SPI,
+                expectedMsgId++,
+                true /* expectedUseEncap */,
+                hexStringToByteArray(SUCCESS_DELETE_CHILD_RESP));
+
+        additionalChildCb.awaitOnClosed();
+
+        // Close IKE Session
+        ikeSession.close();
+        mTunUtils.awaitReqAndInjectResp(
+                IKE_INIT_SPI,
+                expectedMsgId++,
+                true /* expectedUseEncap */,
+                hexStringToByteArray(SUCCESS_DELETE_IKE_RESP));
+
+        mFirstChildSessionCallback.awaitOnClosed();
+        mIkeSessionCallback.awaitOnClosed();
+
+        // TODO: verify IpSecTransform pair is created and deleted
+    }
+
+    @Test
+    public void testIkeSessionKill() throws Exception {
+        // Open IKE Session
+        IkeSession ikeSession = openIkeSession(createIkeSessionParams(mRemoteAddress));
+        int expectedMsgId = 0;
+        mTunUtils.awaitReqAndInjectResp(
+                IKE_INIT_SPI,
+                expectedMsgId++,
+                false /* expectedUseEncap */,
+                hexStringToByteArray(SUCCESS_IKE_INIT_RESP));
+
+        mTunUtils.awaitReqAndInjectResp(
+                IKE_INIT_SPI,
+                expectedMsgId++,
+                true /* expectedUseEncap */,
+                hexStringToByteArray(SUCCESS_IKE_AUTH_RESP));
+
+        ikeSession.kill();
+
+        mFirstChildSessionCallback.awaitOnClosed();
+        mIkeSessionCallback.awaitOnClosed();
+    }
+
+    @Test
+    public void testIkeInitFail() throws Exception {
+        String ikeInitFailRespHex =
+                "46B8ECA1E0D72A180000000000000000292022200000000000000024000000080000000E";
+
+        // Open IKE Session
+        IkeSession ikeSession = openIkeSession(createIkeSessionParams(mRemoteAddress));
+        int expectedMsgId = 0;
+        mTunUtils.awaitReqAndInjectResp(
+                IKE_INIT_SPI,
+                expectedMsgId++,
+                false /* expectedUseEncap */,
+                hexStringToByteArray(ikeInitFailRespHex));
+
+        IkeException exception = mIkeSessionCallback.awaitOnClosedException();
+        assertNotNull(exception);
+        assertTrue(exception instanceof IkeProtocolException);
+        IkeProtocolException protocolException = (IkeProtocolException) exception;
+        assertEquals(ERROR_TYPE_NO_PROPOSAL_CHOSEN, protocolException.getErrorType());
+        assertArrayEquals(EXPECTED_PROTOCOL_ERROR_DATA_NONE, protocolException.getErrorData());
+    }
+
+    // TODO(b/148689509): Verify rekey process and handling IKE_AUTH failure
+}
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..deba8fd
--- /dev/null
+++ b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionTestBase.java
@@ -0,0 +1,374 @@
+/*
+ * 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.app.AppOpsManager.OP_MANAGE_IPSEC_TUNNELS;
+
+import android.annotation.NonNull;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.InetAddresses;
+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.IkeTrafficSelector;
+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 android.util.Log;
+
+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.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
+ */
+@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_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 IkeTrafficSelector EXPECTED_INBOUND_TS =
+            new IkeTrafficSelector(
+                    MIN_PORT, MAX_PORT, EXPECTED_INTERNAL_ADDR, EXPECTED_INTERNAL_ADDR);
+
+    // 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 = (TestNetworkManager) sContext.getSystemService(Context.TEST_NETWORK_SERVICE);
+
+        // 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 tearDownPermissionAfterClass() throws Exception {
+        setAppOp(OP_MANAGE_IPSEC_TUNNELS, false);
+
+        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();
+
+        resetNextAvailableAddress(NEXT_AVAILABLE_IP4_ADDR_LOCAL, INITIAL_AVAILABLE_IP4_ADDR_LOCAL);
+        resetNextAvailableAddress(
+                NEXT_AVAILABLE_IP4_ADDR_REMOTE, INITIAL_AVAILABLE_IP4_ADDR_REMOTE);
+    }
+
+    void setUpTestNetwork(InetAddress localAddr) throws Exception {
+        int prefixLen = localAddr instanceof Inet4Address ? IP4_PREFIX_LEN : IP4_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();
+    }
+
+    private 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
+            Log.d("IKE", "CTS setAppOp cmd " + cmd);
+
+            String result = 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);
+        }
+    }
+
+    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);
+        }
+    }
+
+    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;
+        }
+    }
+
+    // TODO(b/148689509): Verify IKE Session setup using EAP and digital-signature-based auth
+
+    // 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..f07c710 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;
 
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..5a8258d
--- /dev/null
+++ b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeTunUtils.java
@@ -0,0 +1,243 @@
+/*
+ * 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 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.Arrays;
+
+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_HEADER_LEN = 28;
+    private static final int IKE_INIT_SPI_OFFSET = 0;
+    private static final int IKE_IS_RESP_BYTE_OFFSET = 19;
+    private static final int IKE_MSG_ID_OFFSET = 20;
+
+    public IkeTunUtils(ParcelFileDescriptor tunFd) {
+        super(tunFd);
+    }
+
+    /**
+     * Await the expected IKE request and inject an IKE response.
+     *
+     * @param respIkePkt IKE response packet without IP/UDP headers or NON ESP MARKER.
+     */
+    public byte[] awaitReqAndInjectResp(
+            long expectedInitIkeSpi, int expectedMsgId, boolean expectedUseEncap, byte[] respIkePkt)
+            throws Exception {
+        byte[] request =
+                awaitIkePacket(
+                        expectedInitIkeSpi,
+                        expectedMsgId,
+                        false /* expectedResp */,
+                        expectedUseEncap);
+
+        // 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 */);
+
+        byte[] response =
+                buildIkePacket(srcAddr, dstAddr, srcPort, dstPort, expectedUseEncap, respIkePkt);
+        injectPacket(response);
+        return request;
+    }
+
+    private byte[] awaitIkePacket(
+            long expectedInitIkeSpi,
+            int expectedMsgId,
+            boolean expectedResp,
+            boolean expectedUseEncap)
+            throws Exception {
+        long endTime = System.currentTimeMillis() + TIMEOUT;
+        int startIndex = 0;
+        synchronized (mPackets) {
+            while (System.currentTimeMillis() < endTime) {
+                byte[] ikePkt =
+                        getFirstMatchingPacket(
+                                (pkt) -> {
+                                    return isIke(
+                                            pkt,
+                                            expectedInitIkeSpi,
+                                            expectedMsgId,
+                                            expectedResp,
+                                            expectedUseEncap);
+                                },
+                                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);
+                }
+            }
+
+            String direction = expectedResp ? "response" : "request";
+            fail(
+                    "No such IKE "
+                            + direction
+                            + " found with Initiator SPI "
+                            + expectedInitIkeSpi
+                            + " and message ID "
+                            + expectedMsgId);
+        }
+        return null;
+    }
+
+    private static boolean isIke(
+            byte[] pkt,
+            long expectedInitIkeSpi,
+            int expectedMsgId,
+            boolean expectedResp,
+            boolean expectedUseEncap) {
+        int ipProtocolOffset = 0;
+        int ikeOffset = 0;
+        if (isIpv6(pkt)) {
+            // IPv6 UDP expectedUseEncap not supported by kernels; assume non-expectedUseEncap.
+            ipProtocolOffset = IP6_PROTO_OFFSET;
+            ikeOffset = IP6_HDRLEN + UDP_HDRLEN;
+        } else {
+            // Use default IPv4 header length (assuming no options)
+            ipProtocolOffset = IP4_PROTO_OFFSET;
+            ikeOffset = IP4_HDRLEN + UDP_HDRLEN;
+
+            if (expectedUseEncap) {
+                if (hasNonEspMarker(pkt)) {
+                    ikeOffset += NON_ESP_MARKER_LEN;
+                } else {
+                    return false;
+                }
+            }
+        }
+
+        return pkt[ipProtocolOffset] == IPPROTO_UDP
+                && areSpiAndMsgIdEqual(
+                        pkt, ikeOffset, expectedInitIkeSpi, expectedMsgId, expectedResp);
+    }
+
+    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 areSpiAndMsgIdEqual(
+            byte[] pkt,
+            int ikeOffset,
+            long expectedIkeInitSpi,
+            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)
+
+        // Check message ID.
+        buffer.get(new byte[IKE_MSG_ID_OFFSET]);
+        int msgId = buffer.getInt();
+        return expectedMsgId == msgId;
+
+        // TODO: Check SPI and packet direction
+    }
+
+    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());
+    }
+
+    private 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..cb1d826 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 = 100;
 
-    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;
     }