Snap for 6453963 from 28ff52e32efbff9fad525c95291007e13ad5fe76 to rvc-release

Change-Id: I26cbfacc4a5dc3f30f31d88914c2eb7c81e7075c
This commit is contained in:
android-build-team Robot
2020-05-02 01:06:18 +00:00
11 changed files with 1163 additions and 103 deletions

View File

@@ -30,6 +30,7 @@ import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isDozeModeSupp
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 @@ public abstract class AbstractRestrictBackgroundNetworkTestCase {
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 "

View File

@@ -47,6 +47,7 @@ java_defaults {
"mockwebserver",
"junit",
"junit-params",
"libnanohttpd",
"truth-prebuilt",
],

View File

@@ -33,6 +33,7 @@ android_test {
"androidx.test.ext.junit",
"compatibility-device-util-axt",
"ctstestrunner-axt",
"net-tests-utils",
],
platform_apis: true,

View File

@@ -46,6 +46,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
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.Set;
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 @@ public final class IkeSessionParamsTest extends IkeSessionParamsTestBase {
@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 @@ public final class IkeSessionParamsTest extends IkeSessionParamsTestBase {
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 @@ public final class IkeSessionParamsTest extends IkeSessionParamsTestBase {
*/
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 @@ public final class IkeSessionParamsTest extends IkeSessionParamsTestBase {
* @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 @@ public final class IkeSessionParamsTest extends IkeSessionParamsTestBase {
*/
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 @@ public final class IkeSessionParamsTest extends IkeSessionParamsTestBase {
* @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());

View File

@@ -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();
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -31,13 +31,15 @@ import java.util.Map;
/** 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;

View File

@@ -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);
}
}
}

View File

@@ -47,18 +47,18 @@ public class TunUtils {
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 @@ public class TunUtils {
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 @@ public class TunUtils {
}
}
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;
}

View File

@@ -0,0 +1,254 @@
/*
* 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.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.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)
doAsShell(NETWORK_SETTINGS) { 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)
}
}
}