diff --git a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java index 9d357055d1..0248f971dc 100644 --- a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java +++ b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java @@ -17,19 +17,55 @@ package android.net.cts; import static android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback; +import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport; +import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_PROBES_ATTEMPTED_BITMASK; +import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_PROBES_SUCCEEDED_BITMASK; +import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_VALIDATION_RESULT; +import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.NETWORK_VALIDATION_RESULT_VALID; +import static android.net.ConnectivityDiagnosticsManager.DataStallReport; +import static android.net.ConnectivityDiagnosticsManager.DataStallReport.DETECTION_METHOD_DNS_EVENTS; +import static android.net.ConnectivityDiagnosticsManager.DataStallReport.DETECTION_METHOD_TCP_METRICS; +import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_DNS_CONSECUTIVE_TIMEOUTS; +import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS; +import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_TCP_PACKET_FAIL_RATE; +import static android.net.ConnectivityDiagnosticsManager.persistableBundleEquals; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN; +import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED; +import static android.net.NetworkCapabilities.TRANSPORT_TEST; +import static android.net.cts.util.CtsNetUtils.TestNetworkCallback; +import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import android.annotation.NonNull; import android.content.Context; import android.net.ConnectivityDiagnosticsManager; +import android.net.ConnectivityManager; +import android.net.LinkAddress; +import android.net.Network; +import android.net.NetworkCapabilities; import android.net.NetworkRequest; +import android.net.TestNetworkInterface; +import android.net.TestNetworkManager; +import android.os.Binder; import android.os.Build; +import android.os.IBinder; +import android.os.PersistableBundle; +import android.os.Process; +import android.util.Pair; import androidx.test.InstrumentationRegistry; +import com.android.testutils.ArrayTrackRecord; import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; import com.android.testutils.DevSdkIgnoreRunner; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -39,33 +75,77 @@ import java.util.concurrent.Executor; @RunWith(DevSdkIgnoreRunner.class) @IgnoreUpTo(Build.VERSION_CODES.Q) // ConnectivityDiagnosticsManager did not exist in Q public class ConnectivityDiagnosticsManagerTest { + private static final int CALLBACK_TIMEOUT_MILLIS = 5000; + private static final int NO_CALLBACK_INVOKED_TIMEOUT = 500; + private static final long TIMESTAMP = 123456789L; + private static final int DNS_CONSECUTIVE_TIMEOUTS = 5; + private static final int COLLECTION_PERIOD_MILLIS = 5000; + private static final int FAIL_RATE_PERCENTAGE = 100; + private static final int UNKNOWN_DETECTION_METHOD = 4; + private static final int FILTERED_UNKNOWN_DETECTION_METHOD = 0; + private static final Executor INLINE_EXECUTOR = x -> x.run(); - private static final NetworkRequest DEFAULT_REQUEST = new NetworkRequest.Builder().build(); + + private static final NetworkRequest TEST_NETWORK_REQUEST = + new NetworkRequest.Builder() + .addTransportType(TRANSPORT_TEST) + .removeCapability(NET_CAPABILITY_TRUSTED) + .removeCapability(NET_CAPABILITY_NOT_VPN) + .build(); + + // Callback used to keep TestNetworks up when there are no other outstanding NetworkRequests + // for it. + private static final TestNetworkCallback TEST_NETWORK_CALLBACK = new TestNetworkCallback(); + + private static final IBinder BINDER = new Binder(); private Context mContext; + private ConnectivityManager mConnectivityManager; private ConnectivityDiagnosticsManager mCdm; - private ConnectivityDiagnosticsCallback mCallback; + private Network mTestNetwork; @Before public void setUp() throws Exception { mContext = InstrumentationRegistry.getContext(); + mConnectivityManager = mContext.getSystemService(ConnectivityManager.class); mCdm = mContext.getSystemService(ConnectivityDiagnosticsManager.class); - mCallback = new ConnectivityDiagnosticsCallback() {}; + mConnectivityManager.requestNetwork(TEST_NETWORK_REQUEST, TEST_NETWORK_CALLBACK); + } + + @After + public void tearDown() throws Exception { + mConnectivityManager.unregisterNetworkCallback(TEST_NETWORK_CALLBACK); + + if (mTestNetwork != null) { + runWithShellPermissionIdentity(() -> { + final TestNetworkManager tnm = mContext.getSystemService(TestNetworkManager.class); + tnm.teardownTestNetwork(mTestNetwork); + }); + } } @Test - public void testRegisterConnectivityDiagnosticsCallback() { - mCdm.registerConnectivityDiagnosticsCallback(DEFAULT_REQUEST, INLINE_EXECUTOR, mCallback); + public void testRegisterConnectivityDiagnosticsCallback() throws Exception { + mTestNetwork = setUpTestNetwork(); + + final TestConnectivityDiagnosticsCallback cb = new TestConnectivityDiagnosticsCallback(); + mCdm.registerConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST, INLINE_EXECUTOR, cb); + + final String interfaceName = + mConnectivityManager.getLinkProperties(mTestNetwork).getInterfaceName(); + + cb.expectOnConnectivityReportAvailable(mTestNetwork, interfaceName); + cb.assertNoCallback(); } @Test public void testRegisterDuplicateConnectivityDiagnosticsCallback() { - mCdm.registerConnectivityDiagnosticsCallback(DEFAULT_REQUEST, INLINE_EXECUTOR, mCallback); + final TestConnectivityDiagnosticsCallback cb = new TestConnectivityDiagnosticsCallback(); + mCdm.registerConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST, INLINE_EXECUTOR, cb); try { - mCdm.registerConnectivityDiagnosticsCallback( - DEFAULT_REQUEST, INLINE_EXECUTOR, mCallback); + mCdm.registerConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST, INLINE_EXECUTOR, cb); fail("Registering the same callback twice should throw an IllegalArgumentException"); } catch (IllegalArgumentException expected) { } @@ -73,13 +153,235 @@ public class ConnectivityDiagnosticsManagerTest { @Test public void testUnregisterConnectivityDiagnosticsCallback() { - mCdm.registerConnectivityDiagnosticsCallback(DEFAULT_REQUEST, INLINE_EXECUTOR, mCallback); - mCdm.unregisterConnectivityDiagnosticsCallback(mCallback); + final TestConnectivityDiagnosticsCallback cb = new TestConnectivityDiagnosticsCallback(); + mCdm.registerConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST, INLINE_EXECUTOR, cb); + mCdm.unregisterConnectivityDiagnosticsCallback(cb); } @Test public void testUnregisterUnknownConnectivityDiagnosticsCallback() { // Expected to silently ignore the unregister() call - mCdm.unregisterConnectivityDiagnosticsCallback(mCallback); + mCdm.unregisterConnectivityDiagnosticsCallback(new TestConnectivityDiagnosticsCallback()); + } + + @Test + public void testOnConnectivityReportAvailable() throws Exception { + mTestNetwork = setUpTestNetwork(); + + final TestConnectivityDiagnosticsCallback cb = new TestConnectivityDiagnosticsCallback(); + mCdm.registerConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST, INLINE_EXECUTOR, cb); + + final String interfaceName = + mConnectivityManager.getLinkProperties(mTestNetwork).getInterfaceName(); + + cb.expectOnConnectivityReportAvailable(mTestNetwork, interfaceName); + cb.assertNoCallback(); + } + + @Test + public void testOnDataStallSuspected_DnsEvents() throws Exception { + final PersistableBundle extras = new PersistableBundle(); + extras.putInt(KEY_DNS_CONSECUTIVE_TIMEOUTS, DNS_CONSECUTIVE_TIMEOUTS); + + verifyOnDataStallSuspected(DETECTION_METHOD_DNS_EVENTS, TIMESTAMP, extras); + } + + @Test + public void testOnDataStallSuspected_TcpMetrics() throws Exception { + final PersistableBundle extras = new PersistableBundle(); + extras.putInt(KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS, COLLECTION_PERIOD_MILLIS); + extras.putInt(KEY_TCP_PACKET_FAIL_RATE, FAIL_RATE_PERCENTAGE); + + verifyOnDataStallSuspected(DETECTION_METHOD_TCP_METRICS, TIMESTAMP, extras); + } + + @Test + public void testOnDataStallSuspected_UnknownDetectionMethod() throws Exception { + verifyOnDataStallSuspected( + UNKNOWN_DETECTION_METHOD, + FILTERED_UNKNOWN_DETECTION_METHOD, + TIMESTAMP, + PersistableBundle.EMPTY); + } + + private void verifyOnDataStallSuspected( + int detectionMethod, long timestampMillis, @NonNull PersistableBundle extras) + throws Exception { + // Input detection method is expected to match received detection method + verifyOnDataStallSuspected(detectionMethod, detectionMethod, timestampMillis, extras); + } + + private void verifyOnDataStallSuspected( + int inputDetectionMethod, + int expectedDetectionMethod, + long timestampMillis, + @NonNull PersistableBundle extras) + throws Exception { + mTestNetwork = setUpTestNetwork(); + + final TestConnectivityDiagnosticsCallback cb = new TestConnectivityDiagnosticsCallback(); + mCdm.registerConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST, INLINE_EXECUTOR, cb); + + final String interfaceName = + mConnectivityManager.getLinkProperties(mTestNetwork).getInterfaceName(); + + cb.expectOnConnectivityReportAvailable(mTestNetwork, interfaceName); + + runWithShellPermissionIdentity( + () -> mConnectivityManager.simulateDataStall( + inputDetectionMethod, timestampMillis, mTestNetwork, extras), + android.Manifest.permission.MANAGE_TEST_NETWORKS); + + cb.expectOnDataStallSuspected( + mTestNetwork, interfaceName, expectedDetectionMethod, timestampMillis, extras); + cb.assertNoCallback(); + } + + @Test + public void testOnNetworkConnectivityReportedTrue() throws Exception { + verifyOnNetworkConnectivityReported(true /* hasConnectivity */); + } + + @Test + public void testOnNetworkConnectivityReportedFalse() throws Exception { + verifyOnNetworkConnectivityReported(false /* hasConnectivity */); + } + + private void verifyOnNetworkConnectivityReported(boolean hasConnectivity) throws Exception { + mTestNetwork = setUpTestNetwork(); + + final TestConnectivityDiagnosticsCallback cb = new TestConnectivityDiagnosticsCallback(); + mCdm.registerConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST, INLINE_EXECUTOR, cb); + + // onConnectivityReportAvailable always invoked when the test network is established + final String interfaceName = + mConnectivityManager.getLinkProperties(mTestNetwork).getInterfaceName(); + cb.expectOnConnectivityReportAvailable(mTestNetwork, interfaceName); + cb.assertNoCallback(); + + mConnectivityManager.reportNetworkConnectivity(mTestNetwork, hasConnectivity); + + cb.expectOnNetworkConnectivityReported(mTestNetwork, hasConnectivity); + + // if hasConnectivity does not match the network's known connectivity, it will be + // revalidated which will trigger another onConnectivityReportAvailable callback. + if (!hasConnectivity) { + cb.expectOnConnectivityReportAvailable(mTestNetwork, interfaceName); + } + + cb.assertNoCallback(); + } + + @NonNull + private Network waitForConnectivityServiceIdleAndGetNetwork() throws InterruptedException { + // Get a new Network. This requires going through the ConnectivityService thread. Once it + // completes, all previously enqueued messages on the ConnectivityService main Handler have + // completed. + final TestNetworkCallback callback = new TestNetworkCallback(); + mConnectivityManager.requestNetwork(TEST_NETWORK_REQUEST, callback); + final Network network = callback.waitForAvailable(); + mConnectivityManager.unregisterNetworkCallback(callback); + assertNotNull(network); + return network; + } + + /** + * Registers a test NetworkAgent with ConnectivityService with limited capabilities, which leads + * to the Network being validated. + */ + @NonNull + private Network setUpTestNetwork() throws Exception { + final int[] administratorUids = new int[] {Process.myUid()}; + runWithShellPermissionIdentity( + () -> { + final TestNetworkManager tnm = + mContext.getSystemService(TestNetworkManager.class); + final TestNetworkInterface tni = tnm.createTunInterface(new LinkAddress[0]); + tnm.setupTestNetwork(tni.getInterfaceName(), administratorUids, BINDER); + }); + return waitForConnectivityServiceIdleAndGetNetwork(); + } + + private static class TestConnectivityDiagnosticsCallback + extends ConnectivityDiagnosticsCallback { + private final ArrayTrackRecord.ReadHead mHistory = + new ArrayTrackRecord().newReadHead(); + + @Override + public void onConnectivityReportAvailable(ConnectivityReport report) { + mHistory.add(report); + } + + @Override + public void onDataStallSuspected(DataStallReport report) { + mHistory.add(report); + } + + @Override + public void onNetworkConnectivityReported(Network network, boolean hasConnectivity) { + mHistory.add(new Pair(network, hasConnectivity)); + } + + public void expectOnConnectivityReportAvailable( + @NonNull Network network, @NonNull String interfaceName) { + final ConnectivityReport result = + (ConnectivityReport) mHistory.poll(CALLBACK_TIMEOUT_MILLIS, x -> true); + assertEquals(network, result.getNetwork()); + + final NetworkCapabilities nc = result.getNetworkCapabilities(); + assertNotNull(nc); + assertTrue(nc.hasTransport(TRANSPORT_TEST)); + assertNotNull(result.getLinkProperties()); + assertEquals(interfaceName, result.getLinkProperties().getInterfaceName()); + + final PersistableBundle extras = result.getAdditionalInfo(); + assertTrue(extras.containsKey(KEY_NETWORK_VALIDATION_RESULT)); + final int validationResult = extras.getInt(KEY_NETWORK_VALIDATION_RESULT); + assertEquals("Network validation result is not 'valid'", + NETWORK_VALIDATION_RESULT_VALID, validationResult); + + assertTrue(extras.containsKey(KEY_NETWORK_PROBES_SUCCEEDED_BITMASK)); + final int probesSucceeded = extras.getInt(KEY_NETWORK_VALIDATION_RESULT); + assertTrue("PROBES_SUCCEEDED mask not in expected range", probesSucceeded >= 0); + + assertTrue(extras.containsKey(KEY_NETWORK_PROBES_ATTEMPTED_BITMASK)); + final int probesAttempted = extras.getInt(KEY_NETWORK_PROBES_ATTEMPTED_BITMASK); + assertTrue("PROBES_ATTEMPTED mask not in expected range", probesAttempted >= 0); + } + + public void expectOnDataStallSuspected( + @NonNull Network network, + @NonNull String interfaceName, + int detectionMethod, + long timestampMillis, + @NonNull PersistableBundle extras) { + final DataStallReport result = + (DataStallReport) mHistory.poll(CALLBACK_TIMEOUT_MILLIS, x -> true); + assertEquals(network, result.getNetwork()); + assertEquals(detectionMethod, result.getDetectionMethod()); + assertEquals(timestampMillis, result.getReportTimestamp()); + + final NetworkCapabilities nc = result.getNetworkCapabilities(); + assertNotNull(nc); + assertTrue(nc.hasTransport(TRANSPORT_TEST)); + assertNotNull(result.getLinkProperties()); + assertEquals(interfaceName, result.getLinkProperties().getInterfaceName()); + + assertTrue(persistableBundleEquals(extras, result.getStallDetails())); + } + + public void expectOnNetworkConnectivityReported( + @NonNull Network network, boolean hasConnectivity) { + final Pair result = + (Pair) mHistory.poll(CALLBACK_TIMEOUT_MILLIS, x -> true); + assertEquals(network, result.first /* network */); + assertEquals(hasConnectivity, result.second /* hasConnectivity */); + } + + public void assertNoCallback() { + // If no more callbacks exist, there should be nothing left in the ReadHead + assertNull("Unexpected event in history", + mHistory.poll(NO_CALLBACK_INVOKED_TIMEOUT, x -> true)); + } } } diff --git a/tests/cts/net/src/android/net/cts/IkeTunUtils.java b/tests/cts/net/src/android/net/cts/IkeTunUtils.java new file mode 100644 index 0000000000..fc25292b27 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/IkeTunUtils.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.net.cts; + +import static android.net.cts.PacketUtils.BytePayload; +import static android.net.cts.PacketUtils.IP4_HDRLEN; +import static android.net.cts.PacketUtils.IP6_HDRLEN; +import static android.net.cts.PacketUtils.IpHeader; +import static android.net.cts.PacketUtils.UDP_HDRLEN; +import static android.net.cts.PacketUtils.UdpHeader; +import static android.net.cts.PacketUtils.getIpHeader; +import static android.system.OsConstants.IPPROTO_UDP; + +import android.os.ParcelFileDescriptor; + +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.util.Arrays; + +// TODO: Merge this with the version in the IPsec module (IKEv2 library) CTS tests. +/** An extension of the TunUtils class with IKE-specific packet handling. */ +public class IkeTunUtils extends TunUtils { + private static final int PORT_LEN = 2; + + private static final byte[] NON_ESP_MARKER = new byte[] {0, 0, 0, 0}; + + private static final int IKE_HEADER_LEN = 28; + private static final int IKE_SPI_LEN = 8; + private static final int IKE_IS_RESP_BYTE_OFFSET = 19; + private static final int IKE_MSG_ID_OFFSET = 20; + private static final int IKE_MSG_ID_LEN = 4; + + public IkeTunUtils(ParcelFileDescriptor tunFd) { + super(tunFd); + } + + /** + * Await an 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 encapExpected, byte[] respIkePkt) throws Exception { + final byte[] request = awaitIkePacket(expectedInitIkeSpi, expectedMsgId, encapExpected); + + // Build response header by flipping address and port + final InetAddress srcAddr = getDstAddress(request); + final InetAddress dstAddr = getSrcAddress(request); + final int srcPort = getDstPort(request); + final int dstPort = getSrcPort(request); + + final byte[] response = + buildIkePacket(srcAddr, dstAddr, srcPort, dstPort, encapExpected, respIkePkt); + injectPacket(response); + return request; + } + + private byte[] awaitIkePacket(long expectedInitIkeSpi, int expectedMsgId, boolean expectEncap) + throws Exception { + return super.awaitPacket(pkt -> isIke(pkt, expectedInitIkeSpi, expectedMsgId, expectEncap)); + } + + private static boolean isIke( + byte[] pkt, long expectedInitIkeSpi, int expectedMsgId, boolean encapExpected) { + final int ipProtocolOffset; + final int ikeOffset; + + if (isIpv6(pkt)) { + ipProtocolOffset = IP6_PROTO_OFFSET; + ikeOffset = IP6_HDRLEN + UDP_HDRLEN; + } else { + if (encapExpected && !hasNonEspMarkerv4(pkt)) { + return false; + } + + // Use default IPv4 header length (assuming no options) + final int encapMarkerLen = encapExpected ? NON_ESP_MARKER.length : 0; + ipProtocolOffset = IP4_PROTO_OFFSET; + ikeOffset = IP4_HDRLEN + UDP_HDRLEN + encapMarkerLen; + } + + return pkt[ipProtocolOffset] == IPPROTO_UDP + && areSpiAndMsgIdEqual(pkt, ikeOffset, expectedInitIkeSpi, expectedMsgId); + } + + /** Checks if the provided IPv4 packet has a UDP-encapsulation NON-ESP marker */ + private static boolean hasNonEspMarkerv4(byte[] ipv4Pkt) { + final int nonEspMarkerOffset = IP4_HDRLEN + UDP_HDRLEN; + if (ipv4Pkt.length < nonEspMarkerOffset + NON_ESP_MARKER.length) { + return false; + } + + final byte[] nonEspMarker = Arrays.copyOfRange( + ipv4Pkt, nonEspMarkerOffset, nonEspMarkerOffset + NON_ESP_MARKER.length); + return Arrays.equals(NON_ESP_MARKER, nonEspMarker); + } + + private static boolean areSpiAndMsgIdEqual( + byte[] pkt, int ikeOffset, long expectedIkeInitSpi, int expectedMsgId) { + if (pkt.length <= ikeOffset + IKE_HEADER_LEN) { + return false; + } + + final ByteBuffer buffer = ByteBuffer.wrap(pkt); + final long spi = buffer.getLong(ikeOffset); + final int msgId = buffer.getInt(ikeOffset + IKE_MSG_ID_OFFSET); + + return expectedIkeInitSpi == spi && expectedMsgId == msgId; + } + + private static InetAddress getSrcAddress(byte[] pkt) throws Exception { + return getAddress(pkt, true); + } + + private static InetAddress getDstAddress(byte[] pkt) throws Exception { + return getAddress(pkt, false); + } + + private static InetAddress getAddress(byte[] pkt, boolean getSrcAddr) throws Exception { + final int ipLen = isIpv6(pkt) ? IP6_ADDR_LEN : IP4_ADDR_LEN; + final int srcIpOffset = isIpv6(pkt) ? IP6_ADDR_OFFSET : IP4_ADDR_OFFSET; + final int ipOffset = getSrcAddr ? srcIpOffset : srcIpOffset + ipLen; + + if (pkt.length < ipOffset + ipLen) { + // Should be impossible; getAddress() is only called with a full IKE request including + // the IP and UDP headers. + throw new IllegalArgumentException("Packet was too short to contain IP address"); + } + + return InetAddress.getByAddress(Arrays.copyOfRange(pkt, ipOffset, ipOffset + ipLen)); + } + + private static int getSrcPort(byte[] pkt) throws Exception { + return getPort(pkt, true); + } + + private static int getDstPort(byte[] pkt) throws Exception { + return getPort(pkt, false); + } + + private static int getPort(byte[] pkt, boolean getSrcPort) { + final int srcPortOffset = isIpv6(pkt) ? IP6_HDRLEN : IP4_HDRLEN; + final int portOffset = getSrcPort ? srcPortOffset : srcPortOffset + PORT_LEN; + + if (pkt.length < portOffset + PORT_LEN) { + // Should be impossible; getPort() is only called with a full IKE request including the + // IP and UDP headers. + throw new IllegalArgumentException("Packet was too short to contain port"); + } + + final ByteBuffer buffer = ByteBuffer.wrap(pkt); + return Short.toUnsignedInt(buffer.getShort(portOffset)); + } + + private static byte[] buildIkePacket( + InetAddress srcAddr, + InetAddress dstAddr, + int srcPort, + int dstPort, + boolean useEncap, + byte[] payload) + throws Exception { + // Append non-ESP marker if encap is enabled + if (useEncap) { + final ByteBuffer buffer = ByteBuffer.allocate(NON_ESP_MARKER.length + payload.length); + buffer.put(NON_ESP_MARKER); + buffer.put(payload); + payload = buffer.array(); + } + + final UdpHeader udpPkt = new UdpHeader(srcPort, dstPort, new BytePayload(payload)); + final IpHeader ipPkt = getIpHeader(udpPkt.getProtocolId(), srcAddr, dstAddr, udpPkt); + return ipPkt.getPacketBytes(); + } +} diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java new file mode 100644 index 0000000000..5cc0cb4128 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java @@ -0,0 +1,517 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.cts; + +import static android.net.NetworkCapabilities.TRANSPORT_VPN; +import static android.net.cts.util.CtsNetUtils.TestNetworkCallback; + +import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; + +import android.Manifest; +import android.annotation.NonNull; +import android.app.AppOpsManager; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.Ikev2VpnProfile; +import android.net.IpSecAlgorithm; +import android.net.LinkAddress; +import android.net.Network; +import android.net.NetworkRequest; +import android.net.ProxyInfo; +import android.net.TestNetworkInterface; +import android.net.TestNetworkManager; +import android.net.VpnManager; +import android.net.cts.util.CtsNetUtils; +import android.platform.test.annotations.AppModeFull; + +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.HexDump; +import com.android.org.bouncycastle.x509.X509V1CertificateGenerator; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.math.BigInteger; +import java.net.InetAddress; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import javax.security.auth.x500.X500Principal; + +@RunWith(AndroidJUnit4.class) +@AppModeFull(reason = "Appops state changes disallowed for instant apps (OP_ACTIVATE_PLATFORM_VPN)") +public class Ikev2VpnTest { + private static final String TAG = Ikev2VpnTest.class.getSimpleName(); + + // Test vectors for IKE negotiation in test mode. + private static final String SUCCESSFUL_IKE_INIT_RESP_V4 = + "46b8eca1e0d72a18b2b5d9006d47a0022120222000000000000002d0220000300000002c01010004030000" + + "0c0100000c800e0100030000080300000c030000080200000400000008040000102800020800" + + "100000b8070f159fe5141d8754ca86f72ecc28d66f514927e96cbe9eec0adb42bf2c276a0ab7" + + "a97fa93555f4be9218c14e7f286bb28c6b4fb13825a420f2ffc165854f200bab37d69c8963d4" + + "0acb831d983163aa50622fd35c182efe882cf54d6106222abcfaa597255d302f1b95ab71c142" + + "c279ea5839a180070bff73f9d03fab815f0d5ee2adec7e409d1e35979f8bd92ffd8aab13d1a0" + + "0657d816643ae767e9ae84d2ccfa2bcce1a50572be8d3748ae4863c41ae90da16271e014270f" + + "77edd5cd2e3299f3ab27d7203f93d770bacf816041cdcecd0f9af249033979da4369cb242dd9" + + "6d172e60513ff3db02de63e50eb7d7f596ada55d7946cad0af0669d1f3e2804846ab3f2a930d" + + "df56f7f025f25c25ada694e6231abbb87ee8cfd072c8481dc0b0f6b083fdc3bd89b080e49feb" + + "0288eef6fdf8a26ee2fc564a11e7385215cf2deaf2a9965638fc279c908ccdf04094988d91a2" + + "464b4a8c0326533aff5119ed79ecbd9d99a218b44f506a5eb09351e67da86698b4c58718db25" + + "d55f426fb4c76471b27a41fbce00777bc233c7f6e842e39146f466826de94f564cad8b92bfbe" + + "87c99c4c7973ec5f1eea8795e7da82819753aa7c4fcfdab77066c56b939330c4b0d354c23f83" + + "ea82fa7a64c4b108f1188379ea0eb4918ee009d804100e6bf118771b9058d42141c847d5ec37" + + "6e5ec591c71fc9dac01063c2bd31f9c783b28bf1182900002430f3d5de3449462b31dd28bc27" + + "297b6ad169bccce4f66c5399c6e0be9120166f2900001c0000400428b8df2e66f69c8584a186" + + "c5eac66783551d49b72900001c000040054e7a622e802d5cbfb96d5f30a6e433994370173529" + + "0000080000402e290000100000402f00020003000400050000000800004014"; + private static final String SUCCESSFUL_IKE_INIT_RESP_V6 = + "46b8eca1e0d72a1800d9ea1babce26bf2120222000000000000002d0220000300000002c01010004030000" + + "0c0100000c800e0100030000080300000c030000080200000400000008040000102800020800" + + "100000ea0e6dd9ca5930a9a45c323a41f64bfd8cdef7730f5fbff37d7c377da427f489a42aa8" + + "c89233380e6e925990d49de35c2cdcf63a61302c731a4b3569df1ee1bf2457e55a6751838ede" + + "abb75cc63ba5c9e4355e8e784f383a5efe8a44727dc14aeaf8dacc2620fb1c8875416dc07739" + + "7fe4decc1bd514a9c7d270cf21fd734c63a25c34b30b68686e54e8a198f37f27cb491fe27235" + + "fab5476b036d875ccab9a68d65fbf3006197f9bebbf94de0d3802b4fafe1d48d931ce3a1a346" + + "2d65bd639e9bd7fa46299650a9dbaf9b324e40b466942d91a59f41ef8042f8474c4850ed0f63" + + "e9238949d41cd8bbaea9aefdb65443a6405792839563aa5dc5c36b5ce8326ccf8a94d9622b85" + + "038d390d5fc0299e14e1f022966d4ac66515f6108ca04faec44821fe5bbf2ed4f84ff5671219" + + "608cb4c36b44a31ba010c9088f8d5ff943bb9ff857f74be1755f57a5783874adc57f42bb174e" + + "4ad3215de628707014dbcb1707bd214658118fdd7a42b3e1638b991ce5b812a667f1145be811" + + "685e3cd3baf9b18d062657b64c206a4d19a531c252a6a51a04aeaf42c618620cdbab65baca23" + + "82c57ed888422aeaacf7f1bc3fe2247ff7e7eaca218b74d7b31d02f2b0afa123f802529e7e6c" + + "3259d418290740ddbf55686e26998d7edcbbf895664972fed666f2f20af40503aa2af436ec6d" + + "4ec981ab19b9088755d94ae7a7c2066ea331d4e56e290000243fefe5555fce552d57a84e682c" + + "d4a6dfb3f2f94a94464d5bec3d88b88e9559642900001c00004004eb4afff764e7b79bca78b1" + + "3a89100d36d678ae982900001c00004005d177216a3c26f782076e12570d40bfaaa148822929" + + "0000080000402e290000100000402f00020003000400050000000800004014"; + private static final String SUCCESSFUL_IKE_AUTH_RESP_V4 = + "46b8eca1e0d72a18b2b5d9006d47a0022e20232000000001000000e0240000c420a2500a3da4c66fa6929e" + + "600f36349ba0e38de14f78a3ad0416cba8c058735712a3d3f9a0a6ed36de09b5e9e02697e7c4" + + "2d210ac86cfbd709503cfa51e2eab8cfdc6427d136313c072968f6506a546eb5927164200592" + + "6e36a16ee994e63f029432a67bc7d37ca619e1bd6e1678df14853067ecf816b48b81e8746069" + + "406363e5aa55f13cb2afda9dbebee94256c29d630b17dd7f1ee52351f92b6e1c3d8551c513f1" + + "d74ac52a80b2041397e109fe0aeb3c105b0d4be0ae343a943398764281"; + private static final String SUCCESSFUL_IKE_AUTH_RESP_V6 = + "46b8eca1e0d72a1800d9ea1babce26bf2e20232000000001000000f0240000d4aaf6eaa6c06b50447e6f54" + + "827fd8a9d9d6ac8015c1ebb3e8cb03fc6e54b49a107441f50004027cc5021600828026367f03" + + "bc425821cd7772ee98637361300c9b76056e874fea2bd4a17212370b291894264d8c023a01d1" + + "c3b691fd4b7c0b534e8c95af4c4638e2d125cb21c6267e2507cd745d72e8da109c47b9259c6c" + + "57a26f6bc5b337b9b9496d54bdde0333d7a32e6e1335c9ee730c3ecd607a8689aa7b0577b74f" + + "3bf437696a9fd5fc0aee3ed346cd9e15d1dda293df89eb388a8719388a60ca7625754de12cdb" + + "efe4c886c5c401"; + private static final long IKE_INITIATOR_SPI = Long.parseLong("46B8ECA1E0D72A18", 16); + + private static final InetAddress LOCAL_OUTER_4 = InetAddress.parseNumericAddress("192.0.2.1"); + private static final InetAddress LOCAL_OUTER_6 = + InetAddress.parseNumericAddress("2001:db8::1"); + + private static final int IP4_PREFIX_LEN = 32; + private static final int IP6_PREFIX_LEN = 128; + + // TODO: Use IPv6 address when we can generate test vectors (GCE does not allow IPv6 yet). + private static final String TEST_SERVER_ADDR_V4 = "192.0.2.2"; + private static final String TEST_SERVER_ADDR_V6 = "2001:db8::2"; + private static final String TEST_IDENTITY = "client.cts.android.com"; + private static final List TEST_ALLOWED_ALGORITHMS = + Arrays.asList(IpSecAlgorithm.AUTH_CRYPT_AES_GCM); + + private static final ProxyInfo TEST_PROXY_INFO = + ProxyInfo.buildDirectProxy("proxy.cts.android.com", 1234); + private static final int TEST_MTU = 1300; + + private static final byte[] TEST_PSK = "ikeAndroidPsk".getBytes(); + private static final String TEST_USER = "username"; + private static final String TEST_PASSWORD = "pa55w0rd"; + + // Static state to reduce setup/teardown + private static final Context sContext = InstrumentationRegistry.getContext(); + private static final ConnectivityManager sCM = + (ConnectivityManager) sContext.getSystemService(Context.CONNECTIVITY_SERVICE); + private static final VpnManager sVpnMgr = + (VpnManager) sContext.getSystemService(Context.VPN_MANAGEMENT_SERVICE); + private static final CtsNetUtils mCtsNetUtils = new CtsNetUtils(sContext); + + private final X509Certificate mServerRootCa; + private final CertificateAndKey mUserCertKey; + + public Ikev2VpnTest() throws Exception { + // Build certificates + mServerRootCa = generateRandomCertAndKeyPair().cert; + mUserCertKey = generateRandomCertAndKeyPair(); + } + + /** + * Sets the given appop using shell commands + * + *

This method must NEVER be called from within a shell permission, as it will attempt to + * acquire, and then drop the shell permission identity. This results in the caller losing the + * shell permission identity due to these calls not being reference counted. + */ + public void setAppop(int appop, boolean allow) { + // Requires shell permission to update appops. + runWithShellPermissionIdentity(() -> { + mCtsNetUtils.setAppopPrivileged(appop, allow); + }, Manifest.permission.MANAGE_TEST_NETWORKS); + } + + private Ikev2VpnProfile buildIkev2VpnProfileCommon( + Ikev2VpnProfile.Builder builder, boolean isRestrictedToTestNetworks) throws Exception { + if (isRestrictedToTestNetworks) { + builder.restrictToTestNetworks(); + } + + return builder.setBypassable(true) + .setAllowedAlgorithms(TEST_ALLOWED_ALGORITHMS) + .setProxy(TEST_PROXY_INFO) + .setMaxMtu(TEST_MTU) + .setMetered(false) + .build(); + } + + private Ikev2VpnProfile buildIkev2VpnProfilePsk(boolean isRestrictedToTestNetworks) + throws Exception { + return buildIkev2VpnProfilePsk(TEST_SERVER_ADDR_V6, isRestrictedToTestNetworks); + } + + private Ikev2VpnProfile buildIkev2VpnProfilePsk( + String remote, boolean isRestrictedToTestNetworks) throws Exception { + final Ikev2VpnProfile.Builder builder = + new Ikev2VpnProfile.Builder(remote, TEST_IDENTITY).setAuthPsk(TEST_PSK); + + return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks); + } + + private Ikev2VpnProfile buildIkev2VpnProfileUsernamePassword(boolean isRestrictedToTestNetworks) + throws Exception { + final Ikev2VpnProfile.Builder builder = + new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY) + .setAuthUsernamePassword(TEST_USER, TEST_PASSWORD, mServerRootCa); + + return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks); + } + + private Ikev2VpnProfile buildIkev2VpnProfileDigitalSignature(boolean isRestrictedToTestNetworks) + throws Exception { + final Ikev2VpnProfile.Builder builder = + new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY) + .setAuthDigitalSignature( + mUserCertKey.cert, mUserCertKey.key, mServerRootCa); + + return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks); + } + + private void checkBasicIkev2VpnProfile(@NonNull Ikev2VpnProfile profile) throws Exception { + assertEquals(TEST_SERVER_ADDR_V6, profile.getServerAddr()); + assertEquals(TEST_IDENTITY, profile.getUserIdentity()); + assertEquals(TEST_PROXY_INFO, profile.getProxyInfo()); + assertEquals(TEST_ALLOWED_ALGORITHMS, profile.getAllowedAlgorithms()); + assertTrue(profile.isBypassable()); + assertFalse(profile.isMetered()); + assertEquals(TEST_MTU, profile.getMaxMtu()); + assertFalse(profile.isRestrictedToTestNetworks()); + } + + @Test + public void testBuildIkev2VpnProfilePsk() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + + final Ikev2VpnProfile profile = + buildIkev2VpnProfilePsk(false /* isRestrictedToTestNetworks */); + + checkBasicIkev2VpnProfile(profile); + assertArrayEquals(TEST_PSK, profile.getPresharedKey()); + + // Verify nothing else is set. + assertNull(profile.getUsername()); + assertNull(profile.getPassword()); + assertNull(profile.getServerRootCaCert()); + assertNull(profile.getRsaPrivateKey()); + assertNull(profile.getUserCert()); + } + + @Test + public void testBuildIkev2VpnProfileUsernamePassword() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + + final Ikev2VpnProfile profile = + buildIkev2VpnProfileUsernamePassword(false /* isRestrictedToTestNetworks */); + + checkBasicIkev2VpnProfile(profile); + assertEquals(TEST_USER, profile.getUsername()); + assertEquals(TEST_PASSWORD, profile.getPassword()); + assertEquals(mServerRootCa, profile.getServerRootCaCert()); + + // Verify nothing else is set. + assertNull(profile.getPresharedKey()); + assertNull(profile.getRsaPrivateKey()); + assertNull(profile.getUserCert()); + } + + @Test + public void testBuildIkev2VpnProfileDigitalSignature() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + + final Ikev2VpnProfile profile = + buildIkev2VpnProfileDigitalSignature(false /* isRestrictedToTestNetworks */); + + checkBasicIkev2VpnProfile(profile); + assertEquals(mUserCertKey.cert, profile.getUserCert()); + assertEquals(mUserCertKey.key, profile.getRsaPrivateKey()); + assertEquals(mServerRootCa, profile.getServerRootCaCert()); + + // Verify nothing else is set. + assertNull(profile.getUsername()); + assertNull(profile.getPassword()); + assertNull(profile.getPresharedKey()); + } + + private void verifyProvisionVpnProfile( + boolean hasActivateVpn, boolean hasActivatePlatformVpn, boolean expectIntent) + throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + + setAppop(AppOpsManager.OP_ACTIVATE_VPN, hasActivateVpn); + setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, hasActivatePlatformVpn); + + final Ikev2VpnProfile profile = + buildIkev2VpnProfilePsk(false /* isRestrictedToTestNetworks */); + final Intent intent = sVpnMgr.provisionVpnProfile(profile); + assertEquals(expectIntent, intent != null); + } + + @Test + public void testProvisionVpnProfileNoPreviousConsent() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + + verifyProvisionVpnProfile(false /* hasActivateVpn */, + false /* hasActivatePlatformVpn */, true /* expectIntent */); + } + + @Test + public void testProvisionVpnProfilePlatformVpnConsented() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + + verifyProvisionVpnProfile(false /* hasActivateVpn */, + true /* hasActivatePlatformVpn */, false /* expectIntent */); + } + + @Test + public void testProvisionVpnProfileVpnServiceConsented() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + + verifyProvisionVpnProfile(true /* hasActivateVpn */, + false /* hasActivatePlatformVpn */, false /* expectIntent */); + } + + @Test + public void testProvisionVpnProfileAllPreConsented() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + + verifyProvisionVpnProfile(true /* hasActivateVpn */, + true /* hasActivatePlatformVpn */, false /* expectIntent */); + } + + @Test + public void testDeleteVpnProfile() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + + setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, true); + + final Ikev2VpnProfile profile = + buildIkev2VpnProfilePsk(false /* isRestrictedToTestNetworks */); + assertNull(sVpnMgr.provisionVpnProfile(profile)); + + // Verify that deleting the profile works (even without the appop) + setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, false); + sVpnMgr.deleteProvisionedVpnProfile(); + + // Test that the profile was deleted - starting it should throw an IAE. + try { + setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, true); + sVpnMgr.startProvisionedVpnProfile(); + fail("Expected IllegalArgumentException due to missing profile"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testStartVpnProfileNoPreviousConsent() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + + setAppop(AppOpsManager.OP_ACTIVATE_VPN, false); + setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, false); + + // Make sure the VpnProfile is not provisioned already. + sVpnMgr.stopProvisionedVpnProfile(); + + try { + sVpnMgr.startProvisionedVpnProfile(); + fail("Expected SecurityException for missing consent"); + } catch (SecurityException expected) { + } + } + + private void checkStartStopVpnProfileBuildsNetworks(IkeTunUtils tunUtils, boolean testIpv6) + throws Exception { + String serverAddr = testIpv6 ? TEST_SERVER_ADDR_V6 : TEST_SERVER_ADDR_V4; + String initResp = testIpv6 ? SUCCESSFUL_IKE_INIT_RESP_V6 : SUCCESSFUL_IKE_INIT_RESP_V4; + String authResp = testIpv6 ? SUCCESSFUL_IKE_AUTH_RESP_V6 : SUCCESSFUL_IKE_AUTH_RESP_V4; + boolean hasNat = !testIpv6; + + // Requires MANAGE_TEST_NETWORKS to provision a test-mode profile. + mCtsNetUtils.setAppopPrivileged(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, true); + + final Ikev2VpnProfile profile = + buildIkev2VpnProfilePsk(serverAddr, true /* isRestrictedToTestNetworks */); + assertNull(sVpnMgr.provisionVpnProfile(profile)); + + sVpnMgr.startProvisionedVpnProfile(); + + // Inject IKE negotiation + int expectedMsgId = 0; + tunUtils.awaitReqAndInjectResp(IKE_INITIATOR_SPI, expectedMsgId++, false /* isEncap */, + HexDump.hexStringToByteArray(initResp)); + tunUtils.awaitReqAndInjectResp(IKE_INITIATOR_SPI, expectedMsgId++, hasNat /* isEncap */, + HexDump.hexStringToByteArray(authResp)); + + // Verify the VPN network came up + final NetworkRequest nr = new NetworkRequest.Builder() + .clearCapabilities().addTransportType(TRANSPORT_VPN).build(); + + final TestNetworkCallback cb = new TestNetworkCallback(); + sCM.requestNetwork(nr, cb); + cb.waitForAvailable(); + final Network vpnNetwork = cb.currentNetwork; + assertNotNull(vpnNetwork); + + sVpnMgr.stopProvisionedVpnProfile(); + cb.waitForLost(); + assertEquals(vpnNetwork, cb.lastLostNetwork); + } + + private void doTestStartStopVpnProfile(boolean testIpv6) throws Exception { + // Non-final; these variables ensure we clean up properly after our test if we have + // allocated test network resources + final TestNetworkManager tnm = sContext.getSystemService(TestNetworkManager.class); + TestNetworkInterface testIface = null; + TestNetworkCallback tunNetworkCallback = null; + + try { + // Build underlying test network + testIface = tnm.createTunInterface( + new LinkAddress[] { + new LinkAddress(LOCAL_OUTER_4, IP4_PREFIX_LEN), + new LinkAddress(LOCAL_OUTER_6, IP6_PREFIX_LEN)}); + + // Hold on to this callback to ensure network does not get reaped. + tunNetworkCallback = mCtsNetUtils.setupAndGetTestNetwork( + testIface.getInterfaceName()); + final IkeTunUtils tunUtils = new IkeTunUtils(testIface.getFileDescriptor()); + + checkStartStopVpnProfileBuildsNetworks(tunUtils, testIpv6); + } finally { + // Make sure to stop the VPN profile. This is safe to call multiple times. + sVpnMgr.stopProvisionedVpnProfile(); + + if (testIface != null) { + testIface.getFileDescriptor().close(); + } + + if (tunNetworkCallback != null) { + sCM.unregisterNetworkCallback(tunNetworkCallback); + } + + final Network testNetwork = tunNetworkCallback.currentNetwork; + if (testNetwork != null) { + tnm.teardownTestNetwork(testNetwork); + } + } + } + + @Test + public void testStartStopVpnProfileV4() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + + // Requires shell permission to update appops. + runWithShellPermissionIdentity(() -> { + doTestStartStopVpnProfile(false); + }); + } + + @Test + public void testStartStopVpnProfileV6() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + + // Requires shell permission to update appops. + runWithShellPermissionIdentity(() -> { + doTestStartStopVpnProfile(true); + }); + } + + private static class CertificateAndKey { + public final X509Certificate cert; + public final PrivateKey key; + + CertificateAndKey(X509Certificate cert, PrivateKey key) { + this.cert = cert; + this.key = key; + } + } + + private static CertificateAndKey generateRandomCertAndKeyPair() throws Exception { + final Date validityBeginDate = + new Date(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1L)); + final Date validityEndDate = + new Date(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1L)); + + // Generate a keypair + final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(512); + final KeyPair keyPair = keyPairGenerator.generateKeyPair(); + + final X500Principal dnName = new X500Principal("CN=test.android.com"); + final X509V1CertificateGenerator certGen = new X509V1CertificateGenerator(); + certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis())); + certGen.setSubjectDN(dnName); + certGen.setIssuerDN(dnName); + certGen.setNotBefore(validityBeginDate); + certGen.setNotAfter(validityEndDate); + certGen.setPublicKey(keyPair.getPublic()); + certGen.setSignatureAlgorithm("SHA256WithRSAEncryption"); + + final X509Certificate cert = certGen.generate(keyPair.getPrivate(), "AndroidOpenSSL"); + return new CertificateAndKey(cert, keyPair.getPrivate()); + } +} diff --git a/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java b/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java index 1d83dda33c..ae38faa124 100644 --- a/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java +++ b/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java @@ -18,20 +18,17 @@ package android.net.cts; import static android.app.AppOpsManager.OP_MANAGE_IPSEC_TUNNELS; import static android.net.IpSecManager.UdpEncapsulationSocket; -import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN; -import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED; -import static android.net.NetworkCapabilities.TRANSPORT_TEST; import static android.net.cts.PacketUtils.AES_CBC_BLK_SIZE; import static android.net.cts.PacketUtils.AES_CBC_IV_LEN; import static android.net.cts.PacketUtils.BytePayload; import static android.net.cts.PacketUtils.EspHeader; import static android.net.cts.PacketUtils.IP4_HDRLEN; import static android.net.cts.PacketUtils.IP6_HDRLEN; -import static android.net.cts.PacketUtils.Ip4Header; -import static android.net.cts.PacketUtils.Ip6Header; import static android.net.cts.PacketUtils.IpHeader; import static android.net.cts.PacketUtils.UDP_HDRLEN; import static android.net.cts.PacketUtils.UdpHeader; +import static android.net.cts.PacketUtils.getIpHeader; +import static android.net.cts.util.CtsNetUtils.TestNetworkCallback; import static android.system.OsConstants.AF_INET; import static android.system.OsConstants.AF_INET6; @@ -40,38 +37,28 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; -import android.app.AppOpsManager; import android.content.Context; -import android.content.pm.PackageManager; import android.net.ConnectivityManager; import android.net.IpSecAlgorithm; import android.net.IpSecManager; import android.net.IpSecTransform; import android.net.LinkAddress; import android.net.Network; -import android.net.NetworkRequest; import android.net.TestNetworkInterface; import android.net.TestNetworkManager; import android.net.cts.PacketUtils.Payload; -import android.os.Binder; -import android.os.Build; -import android.os.IBinder; +import android.net.cts.util.CtsNetUtils; import android.os.ParcelFileDescriptor; -import android.os.SystemProperties; import android.platform.test.annotations.AppModeFull; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; -import com.android.compatibility.common.util.SystemUtil; - -import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.NetworkInterface; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; import org.junit.AfterClass; import org.junit.Before; @@ -114,7 +101,7 @@ public class IpSecManagerTunnelTest extends IpSecBaseTest { private static TunUtils sTunUtils; private static Context sContext = InstrumentationRegistry.getContext(); - private static IBinder sBinder = new Binder(); + private static final CtsNetUtils mCtsNetUtils = new CtsNetUtils(sContext); @BeforeClass public static void setUpBeforeClass() throws Exception { @@ -127,7 +114,7 @@ public class IpSecManagerTunnelTest extends IpSecBaseTest { // 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); + mCtsNetUtils.setAppopPrivileged(OP_MANAGE_IPSEC_TUNNELS, true); TestNetworkInterface testIface = sTNM.createTunInterface( @@ -137,8 +124,9 @@ public class IpSecManagerTunnelTest extends IpSecBaseTest { }); sTunFd = testIface.getFileDescriptor(); - sTunNetworkCallback = setupAndGetTestNetwork(testIface.getInterfaceName()); - sTunNetwork = sTunNetworkCallback.getNetworkBlocking(); + sTunNetworkCallback = mCtsNetUtils.setupAndGetTestNetwork(testIface.getInterfaceName()); + sTunNetworkCallback.waitForAvailable(); + sTunNetwork = sTunNetworkCallback.currentNetwork; sTunUtils = new TunUtils(sTunFd); } @@ -149,7 +137,7 @@ public class IpSecManagerTunnelTest extends IpSecBaseTest { super.setUp(); // Set to true before every run; some tests flip this. - setAppop(OP_MANAGE_IPSEC_TUNNELS, true); + mCtsNetUtils.setAppopPrivileged(OP_MANAGE_IPSEC_TUNNELS, true); // Clear sTunUtils state sTunUtils.reset(); @@ -157,7 +145,7 @@ public class IpSecManagerTunnelTest extends IpSecBaseTest { @AfterClass public static void tearDownAfterClass() throws Exception { - setAppop(OP_MANAGE_IPSEC_TUNNELS, false); + mCtsNetUtils.setAppopPrivileged(OP_MANAGE_IPSEC_TUNNELS, false); sCM.unregisterNetworkCallback(sTunNetworkCallback); @@ -169,50 +157,12 @@ public class IpSecManagerTunnelTest extends IpSecBaseTest { .dropShellPermissionIdentity(); } - private static boolean hasTunnelsFeature() { - return sContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS) - || SystemProperties.getInt("ro.product.first_api_level", 0) - >= Build.VERSION_CODES.Q; - } - - 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 - SystemUtil.runShellCommand(cmd); - } - } - - private static TestNetworkCallback setupAndGetTestNetwork(String ifname) throws Exception { - // Build a network request - NetworkRequest nr = - new NetworkRequest.Builder() - .clearCapabilities() - .addTransportType(TRANSPORT_TEST) - .setNetworkSpecifier(ifname) - .build(); - - TestNetworkCallback cb = new TestNetworkCallback(); - sCM.requestNetwork(nr, cb); - - // Setup the test network after network request is filed to prevent Network from being - // reaped due to no requests matching it. - sTNM.setupTestNetwork(ifname, sBinder); - - return cb; - } - @Test public void testSecurityExceptionCreateTunnelInterfaceWithoutAppop() throws Exception { - if (!hasTunnelsFeature()) return; + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); // Ensure we don't have the appop. Permission is not requested in the Manifest - setAppop(OP_MANAGE_IPSEC_TUNNELS, false); + mCtsNetUtils.setAppopPrivileged(OP_MANAGE_IPSEC_TUNNELS, false); // Security exceptions are thrown regardless of IPv4/IPv6. Just test one try { @@ -224,10 +174,10 @@ public class IpSecManagerTunnelTest extends IpSecBaseTest { @Test public void testSecurityExceptionBuildTunnelTransformWithoutAppop() throws Exception { - if (!hasTunnelsFeature()) return; + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); // Ensure we don't have the appop. Permission is not requested in the Manifest - setAppop(OP_MANAGE_IPSEC_TUNNELS, false); + mCtsNetUtils.setAppopPrivileged(OP_MANAGE_IPSEC_TUNNELS, false); // Security exceptions are thrown regardless of IPv4/IPv6. Just test one try (IpSecManager.SecurityParameterIndex spi = @@ -253,19 +203,6 @@ public class IpSecManagerTunnelTest extends IpSecBaseTest { public abstract int run(Network ipsecNetwork) throws Exception; } - private static class TestNetworkCallback extends ConnectivityManager.NetworkCallback { - private final CompletableFuture futureNetwork = new CompletableFuture<>(); - - @Override - public void onAvailable(Network network) { - futureNetwork.complete(network); - } - - public Network getNetworkBlocking() throws Exception { - return futureNetwork.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); - } - } - private int getPacketSize( int innerFamily, int outerFamily, boolean useEncap, boolean transportInTunnelMode) { int expectedPacketSize = TEST_DATA.length + UDP_HDRLEN; @@ -499,8 +436,6 @@ public class IpSecManagerTunnelTest extends IpSecBaseTest { public void checkTunnelReflected( int innerFamily, int outerFamily, boolean useEncap, boolean transportInTunnelMode) throws Exception { - if (!hasTunnelsFeature()) return; - InetAddress localInner = innerFamily == AF_INET ? LOCAL_INNER_4 : LOCAL_INNER_6; InetAddress remoteInner = innerFamily == AF_INET ? REMOTE_INNER_4 : REMOTE_INNER_6; @@ -580,7 +515,6 @@ public class IpSecManagerTunnelTest extends IpSecBaseTest { boolean transportInTunnelMode, IpSecTunnelTestRunnableFactory factory) throws Exception { - if (!hasTunnelsFeature()) return; InetAddress localInner = innerFamily == AF_INET ? LOCAL_INNER_4 : LOCAL_INNER_6; InetAddress remoteInner = innerFamily == AF_INET ? REMOTE_INNER_4 : REMOTE_INNER_6; @@ -648,8 +582,9 @@ public class IpSecManagerTunnelTest extends IpSecBaseTest { mISM.createIpSecTunnelInterface(localOuter, remoteOuter, sTunNetwork)) { // Build the test network tunnelIface.addAddress(localInner, innerPrefixLen); - testNetworkCb = setupAndGetTestNetwork(tunnelIface.getInterfaceName()); - Network testNetwork = testNetworkCb.getNetworkBlocking(); + testNetworkCb = mCtsNetUtils.setupAndGetTestNetwork(tunnelIface.getInterfaceName()); + testNetworkCb.waitForAvailable(); + Network testNetwork = testNetworkCb.currentNetwork; // Check interface was created assertNotNull(NetworkInterface.getByName(tunnelIface.getInterfaceName())); @@ -718,18 +653,6 @@ public class IpSecManagerTunnelTest extends IpSecBaseTest { } } - private 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); - } - } - private EspHeader buildTransportModeEspPacket( int spi, InetAddress src, InetAddress dst, int port, Payload payload) throws Exception { IpHeader preEspIpHeader = getIpHeader(payload.getProtocolId(), src, dst, payload); @@ -819,134 +742,158 @@ public class IpSecManagerTunnelTest extends IpSecBaseTest { // Transport-in-Tunnel mode tests @Test public void testTransportInTunnelModeV4InV4() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); checkTunnelOutput(AF_INET, AF_INET, false, true); checkTunnelInput(AF_INET, AF_INET, false, true); } @Test public void testTransportInTunnelModeV4InV4Reflected() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); checkTunnelReflected(AF_INET, AF_INET, false, true); } @Test public void testTransportInTunnelModeV4InV4UdpEncap() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); checkTunnelOutput(AF_INET, AF_INET, true, true); checkTunnelInput(AF_INET, AF_INET, true, true); } @Test public void testTransportInTunnelModeV4InV4UdpEncapReflected() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); checkTunnelReflected(AF_INET, AF_INET, false, true); } @Test public void testTransportInTunnelModeV4InV6() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); checkTunnelOutput(AF_INET, AF_INET6, false, true); checkTunnelInput(AF_INET, AF_INET6, false, true); } @Test public void testTransportInTunnelModeV4InV6Reflected() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); checkTunnelReflected(AF_INET, AF_INET, false, true); } @Test public void testTransportInTunnelModeV6InV4() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); checkTunnelOutput(AF_INET6, AF_INET, false, true); checkTunnelInput(AF_INET6, AF_INET, false, true); } @Test public void testTransportInTunnelModeV6InV4Reflected() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); checkTunnelReflected(AF_INET, AF_INET, false, true); } @Test public void testTransportInTunnelModeV6InV4UdpEncap() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); checkTunnelOutput(AF_INET6, AF_INET, true, true); checkTunnelInput(AF_INET6, AF_INET, true, true); } @Test public void testTransportInTunnelModeV6InV4UdpEncapReflected() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); checkTunnelReflected(AF_INET, AF_INET, false, true); } @Test public void testTransportInTunnelModeV6InV6() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); checkTunnelOutput(AF_INET, AF_INET6, false, true); checkTunnelInput(AF_INET, AF_INET6, false, true); } @Test public void testTransportInTunnelModeV6InV6Reflected() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); checkTunnelReflected(AF_INET, AF_INET, false, true); } // Tunnel mode tests @Test public void testTunnelV4InV4() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); checkTunnelOutput(AF_INET, AF_INET, false, false); checkTunnelInput(AF_INET, AF_INET, false, false); } @Test public void testTunnelV4InV4Reflected() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); checkTunnelReflected(AF_INET, AF_INET, false, false); } @Test public void testTunnelV4InV4UdpEncap() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); checkTunnelOutput(AF_INET, AF_INET, true, false); checkTunnelInput(AF_INET, AF_INET, true, false); } @Test public void testTunnelV4InV4UdpEncapReflected() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); checkTunnelReflected(AF_INET, AF_INET, true, false); } @Test public void testTunnelV4InV6() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); checkTunnelOutput(AF_INET, AF_INET6, false, false); checkTunnelInput(AF_INET, AF_INET6, false, false); } @Test public void testTunnelV4InV6Reflected() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); checkTunnelReflected(AF_INET, AF_INET6, false, false); } @Test public void testTunnelV6InV4() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); checkTunnelOutput(AF_INET6, AF_INET, false, false); checkTunnelInput(AF_INET6, AF_INET, false, false); } @Test public void testTunnelV6InV4Reflected() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); checkTunnelReflected(AF_INET6, AF_INET, false, false); } @Test public void testTunnelV6InV4UdpEncap() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); checkTunnelOutput(AF_INET6, AF_INET, true, false); checkTunnelInput(AF_INET6, AF_INET, true, false); } @Test public void testTunnelV6InV4UdpEncapReflected() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); checkTunnelReflected(AF_INET6, AF_INET, true, false); } @Test public void testTunnelV6InV6() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); checkTunnelOutput(AF_INET6, AF_INET6, false, false); checkTunnelInput(AF_INET6, AF_INET6, false, false); } @Test public void testTunnelV6InV6Reflected() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); checkTunnelReflected(AF_INET6, AF_INET6, false, false); } } diff --git a/tests/cts/net/src/android/net/cts/PacketUtils.java b/tests/cts/net/src/android/net/cts/PacketUtils.java index 6177827ba6..0aedecb5ad 100644 --- a/tests/cts/net/src/android/net/cts/PacketUtils.java +++ b/tests/cts/net/src/android/net/cts/PacketUtils.java @@ -27,6 +27,7 @@ import java.nio.ShortBuffer; import java.security.GeneralSecurityException; import java.security.SecureRandom; import java.util.Arrays; + import javax.crypto.Cipher; import javax.crypto.Mac; import javax.crypto.spec.IvParameterSpec; @@ -443,6 +444,19 @@ public class PacketUtils { return Arrays.copyOfRange(buffer.array(), 0, buffer.position()); } + public 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); + } + } + /* * Debug printing */ diff --git a/tests/cts/net/src/android/net/cts/TunUtils.java b/tests/cts/net/src/android/net/cts/TunUtils.java index a0307137a5..adaba9d398 100644 --- a/tests/cts/net/src/android/net/cts/TunUtils.java +++ b/tests/cts/net/src/android/net/cts/TunUtils.java @@ -21,8 +21,8 @@ import static android.net.cts.PacketUtils.IP6_HDRLEN; import static android.net.cts.PacketUtils.IPPROTO_ESP; import static android.net.cts.PacketUtils.UDP_HDRLEN; import static android.system.OsConstants.IPPROTO_UDP; + import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.fail; import android.os.ParcelFileDescriptor; @@ -39,19 +39,18 @@ import java.util.function.Predicate; public class TunUtils { private static final String TAG = TunUtils.class.getSimpleName(); + protected static final int IP4_ADDR_OFFSET = 12; + protected static final int IP4_ADDR_LEN = 4; + protected static final int IP6_ADDR_OFFSET = 8; + protected static final int IP6_ADDR_LEN = 16; + protected static final int IP4_PROTO_OFFSET = 9; + protected static final int IP6_PROTO_OFFSET = 6; + private static final int DATA_BUFFER_LEN = 4096; - private static final int TIMEOUT = 100; + private static final int TIMEOUT = 1000; - private static final int IP4_PROTO_OFFSET = 9; - private 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; - - private final ParcelFileDescriptor mTunFd; private final List mPackets = new ArrayList<>(); + private final ParcelFileDescriptor mTunFd; private final Thread mReaderThread; public TunUtils(ParcelFileDescriptor tunFd) { @@ -112,46 +111,15 @@ public class TunUtils { return null; } - /** - * Checks if the specified bytes were ever sent in plaintext. - * - *

Only checks for known plaintext bytes to prevent triggering on ICMP/RA packets or the like - * - * @param plaintext the plaintext bytes to check for - * @param startIndex the index in the list to check for - */ - public boolean hasPlaintextPacket(byte[] plaintext, int startIndex) { - Predicate verifier = - (pkt) -> { - return Collections.indexOfSubList(Arrays.asList(pkt), Arrays.asList(plaintext)) - != -1; - }; - return getFirstMatchingPacket(verifier, startIndex) != null; - } - - public byte[] getEspPacket(int spi, boolean encap, int startIndex) { - return getFirstMatchingPacket( - (pkt) -> { - return isEsp(pkt, spi, encap); - }, - startIndex); - } - - public byte[] awaitEspPacketNoPlaintext( - int spi, byte[] plaintext, boolean useEncap, int expectedPacketSize) throws Exception { + protected byte[] awaitPacket(Predicate verifier) throws Exception { long endTime = System.currentTimeMillis() + TIMEOUT; int startIndex = 0; synchronized (mPackets) { while (System.currentTimeMillis() < endTime) { - byte[] espPkt = getEspPacket(spi, useEncap, startIndex); - if (espPkt != null) { - // Validate packet size - assertEquals(expectedPacketSize, espPkt.length); - - // Always check plaintext from start - assertFalse(hasPlaintextPacket(plaintext, 0)); - return espPkt; // We've found the packet we're looking for. + final byte[] pkt = getFirstMatchingPacket(verifier, startIndex); + if (pkt != null) { + return pkt; // We've found the packet we're looking for. } startIndex = mPackets.size(); @@ -162,10 +130,21 @@ public class TunUtils { mPackets.wait(waitTimeout); } } - - fail("No such ESP packet found with SPI " + spi); } - return null; + + fail("No packet found matching verifier"); + throw new IllegalStateException("Impossible condition; should have thrown in fail()"); + } + + public byte[] awaitEspPacketNoPlaintext( + int spi, byte[] plaintext, boolean useEncap, int expectedPacketSize) throws Exception { + final byte[] espPkt = awaitPacket( + (pkt) -> isEspFailIfSpecifiedPlaintextFound(pkt, spi, useEncap, plaintext)); + + // Validate packet size + assertEquals(expectedPacketSize, espPkt.length); + + return espPkt; // We've found the packet we're looking for. } private static boolean isSpiEqual(byte[] pkt, int espOffset, int spi) { @@ -176,6 +155,24 @@ public class TunUtils { && pkt[espOffset + 3] == (byte) (spi & 0xff); } + /** + * Variant of isEsp that also fails the test if the provided plaintext is found + * + * @param pkt the packet bytes to verify + * @param spi the expected SPI to look for + * @param encap whether encap was enabled, and the packet has a UDP header + * @param plaintext the plaintext packet before outbound encryption, which MUST not appear in + * the provided packet. + */ + private static boolean isEspFailIfSpecifiedPlaintextFound( + byte[] pkt, int spi, boolean encap, byte[] plaintext) { + if (Collections.indexOfSubList(Arrays.asList(pkt), Arrays.asList(plaintext)) != -1) { + fail("Banned plaintext packet found"); + } + + return isEsp(pkt, spi, encap); + } + private static boolean isEsp(byte[] pkt, int spi, boolean encap) { if (isIpv6(pkt)) { // IPv6 UDP encap not supported by kernels; assume non-encap. @@ -191,7 +188,7 @@ public class TunUtils { } } - private static boolean isIpv6(byte[] pkt) { + public static boolean isIpv6(byte[] pkt) { // First nibble shows IP version. 0x60 for IPv6 return (pkt[0] & (byte) 0xF0) == (byte) 0x60; } diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java index f39b184914..32cdf92144 100644 --- a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java +++ b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java @@ -20,6 +20,7 @@ import static android.Manifest.permission.NETWORK_SETTINGS; import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OPPORTUNISTIC; import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_TEST; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -27,11 +28,13 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import android.annotation.NonNull; +import android.app.AppOpsManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.ContentResolver; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.PackageManager; import android.net.ConnectivityManager; import android.net.ConnectivityManager.NetworkCallback; import android.net.LinkProperties; @@ -40,7 +43,12 @@ import android.net.NetworkCapabilities; import android.net.NetworkInfo; import android.net.NetworkInfo.State; import android.net.NetworkRequest; +import android.net.TestNetworkManager; import android.net.wifi.WifiManager; +import android.os.Binder; +import android.os.Build; +import android.os.IBinder; +import android.os.SystemProperties; import android.provider.Settings; import android.system.Os; import android.system.OsConstants; @@ -73,6 +81,7 @@ public final class CtsNetUtils { public static final String NETWORK_CALLBACK_ACTION = "ConnectivityManagerTest.NetworkCallbackAction"; + private final IBinder mBinder = new Binder(); private final Context mContext; private final ConnectivityManager mCm; private final ContentResolver mCR; @@ -88,6 +97,51 @@ public final class CtsNetUtils { mCR = context.getContentResolver(); } + /** Checks if FEATURE_IPSEC_TUNNELS is enabled on the device */ + public boolean hasIpsecTunnelsFeature() { + return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS) + || SystemProperties.getInt("ro.product.first_api_level", 0) + >= Build.VERSION_CODES.Q; + } + + /** + * Sets the given appop using shell commands + * + *

Expects caller to hold the shell permission identity. + */ + public void setAppopPrivileged(int appop, boolean allow) { + final String opName = AppOpsManager.opToName(appop); + for (final String pkg : new String[] {"com.android.shell", mContext.getPackageName()}) { + final String cmd = + String.format( + "appops set %s %s %s", + pkg, // Package name + opName, // Appop + (allow ? "allow" : "deny")); // Action + SystemUtil.runShellCommand(cmd); + } + } + + /** Sets up a test network using the provided interface name */ + public TestNetworkCallback setupAndGetTestNetwork(String ifname) throws Exception { + // Build a network request + final NetworkRequest nr = + new NetworkRequest.Builder() + .clearCapabilities() + .addTransportType(TRANSPORT_TEST) + .setNetworkSpecifier(ifname) + .build(); + + final TestNetworkCallback cb = new TestNetworkCallback(); + mCm.requestNetwork(nr, cb); + + // Setup the test network after network request is filed to prevent Network from being + // reaped due to no requests matching it. + mContext.getSystemService(TestNetworkManager.class).setupTestNetwork(ifname, mBinder); + + return cb; + } + // Toggle WiFi twice, leaving it in the state it started in public void toggleWifi() { if (mWifiManager.isWifiEnabled()) {