Merge changes Ic0f71df7,Ib6635f00,Ia5d35c32,I9aeafa45 into rvc-dev
* changes: Add IPv6 testing for IKEv2 VPN tests Add Ikev2VpnTests including IKE negotiation. Add basic tests for IKEv2/IPsec VPNs Extract IPsec and test network utility methods
This commit is contained in:
committed by
Android (Google) Code Review
commit
477e59f84a
188
tests/cts/net/src/android/net/cts/IkeTunUtils.java
Normal file
188
tests/cts/net/src/android/net/cts/IkeTunUtils.java
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
517
tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
Normal file
517
tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
Normal file
@@ -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<String> 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
|
||||||
|
*
|
||||||
|
* <p>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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,20 +18,17 @@ package android.net.cts;
|
|||||||
|
|
||||||
import static android.app.AppOpsManager.OP_MANAGE_IPSEC_TUNNELS;
|
import static android.app.AppOpsManager.OP_MANAGE_IPSEC_TUNNELS;
|
||||||
import static android.net.IpSecManager.UdpEncapsulationSocket;
|
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_BLK_SIZE;
|
||||||
import static android.net.cts.PacketUtils.AES_CBC_IV_LEN;
|
import static android.net.cts.PacketUtils.AES_CBC_IV_LEN;
|
||||||
import static android.net.cts.PacketUtils.BytePayload;
|
import static android.net.cts.PacketUtils.BytePayload;
|
||||||
import static android.net.cts.PacketUtils.EspHeader;
|
import static android.net.cts.PacketUtils.EspHeader;
|
||||||
import static android.net.cts.PacketUtils.IP4_HDRLEN;
|
import static android.net.cts.PacketUtils.IP4_HDRLEN;
|
||||||
import static android.net.cts.PacketUtils.IP6_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.IpHeader;
|
||||||
import static android.net.cts.PacketUtils.UDP_HDRLEN;
|
import static android.net.cts.PacketUtils.UDP_HDRLEN;
|
||||||
import static android.net.cts.PacketUtils.UdpHeader;
|
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_INET;
|
||||||
import static android.system.OsConstants.AF_INET6;
|
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.assertNotNull;
|
||||||
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
|
import static org.junit.Assume.assumeTrue;
|
||||||
|
|
||||||
import android.app.AppOpsManager;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
import android.net.IpSecAlgorithm;
|
import android.net.IpSecAlgorithm;
|
||||||
import android.net.IpSecManager;
|
import android.net.IpSecManager;
|
||||||
import android.net.IpSecTransform;
|
import android.net.IpSecTransform;
|
||||||
import android.net.LinkAddress;
|
import android.net.LinkAddress;
|
||||||
import android.net.Network;
|
import android.net.Network;
|
||||||
import android.net.NetworkRequest;
|
|
||||||
import android.net.TestNetworkInterface;
|
import android.net.TestNetworkInterface;
|
||||||
import android.net.TestNetworkManager;
|
import android.net.TestNetworkManager;
|
||||||
import android.net.cts.PacketUtils.Payload;
|
import android.net.cts.PacketUtils.Payload;
|
||||||
import android.os.Binder;
|
import android.net.cts.util.CtsNetUtils;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.os.SystemProperties;
|
|
||||||
import android.platform.test.annotations.AppModeFull;
|
import android.platform.test.annotations.AppModeFull;
|
||||||
|
|
||||||
import androidx.test.InstrumentationRegistry;
|
import androidx.test.InstrumentationRegistry;
|
||||||
import androidx.test.runner.AndroidJUnit4;
|
import androidx.test.runner.AndroidJUnit4;
|
||||||
|
|
||||||
import com.android.compatibility.common.util.SystemUtil;
|
|
||||||
|
|
||||||
import java.net.Inet4Address;
|
|
||||||
import java.net.Inet6Address;
|
import java.net.Inet6Address;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.NetworkInterface;
|
import java.net.NetworkInterface;
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import org.junit.AfterClass;
|
import org.junit.AfterClass;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
@@ -114,7 +101,7 @@ public class IpSecManagerTunnelTest extends IpSecBaseTest {
|
|||||||
private static TunUtils sTunUtils;
|
private static TunUtils sTunUtils;
|
||||||
|
|
||||||
private static Context sContext = InstrumentationRegistry.getContext();
|
private static Context sContext = InstrumentationRegistry.getContext();
|
||||||
private static IBinder sBinder = new Binder();
|
private static final CtsNetUtils mCtsNetUtils = new CtsNetUtils(sContext);
|
||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
public static void setUpBeforeClass() throws Exception {
|
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
|
// 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
|
// a standard permission is insufficient. So we shell out the appop, to give us the
|
||||||
// right appop permissions.
|
// right appop permissions.
|
||||||
setAppop(OP_MANAGE_IPSEC_TUNNELS, true);
|
mCtsNetUtils.setAppopPrivileged(OP_MANAGE_IPSEC_TUNNELS, true);
|
||||||
|
|
||||||
TestNetworkInterface testIface =
|
TestNetworkInterface testIface =
|
||||||
sTNM.createTunInterface(
|
sTNM.createTunInterface(
|
||||||
@@ -137,8 +124,9 @@ public class IpSecManagerTunnelTest extends IpSecBaseTest {
|
|||||||
});
|
});
|
||||||
|
|
||||||
sTunFd = testIface.getFileDescriptor();
|
sTunFd = testIface.getFileDescriptor();
|
||||||
sTunNetworkCallback = setupAndGetTestNetwork(testIface.getInterfaceName());
|
sTunNetworkCallback = mCtsNetUtils.setupAndGetTestNetwork(testIface.getInterfaceName());
|
||||||
sTunNetwork = sTunNetworkCallback.getNetworkBlocking();
|
sTunNetworkCallback.waitForAvailable();
|
||||||
|
sTunNetwork = sTunNetworkCallback.currentNetwork;
|
||||||
|
|
||||||
sTunUtils = new TunUtils(sTunFd);
|
sTunUtils = new TunUtils(sTunFd);
|
||||||
}
|
}
|
||||||
@@ -149,7 +137,7 @@ public class IpSecManagerTunnelTest extends IpSecBaseTest {
|
|||||||
super.setUp();
|
super.setUp();
|
||||||
|
|
||||||
// Set to true before every run; some tests flip this.
|
// 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
|
// Clear sTunUtils state
|
||||||
sTunUtils.reset();
|
sTunUtils.reset();
|
||||||
@@ -157,7 +145,7 @@ public class IpSecManagerTunnelTest extends IpSecBaseTest {
|
|||||||
|
|
||||||
@AfterClass
|
@AfterClass
|
||||||
public static void tearDownAfterClass() throws Exception {
|
public static void tearDownAfterClass() throws Exception {
|
||||||
setAppop(OP_MANAGE_IPSEC_TUNNELS, false);
|
mCtsNetUtils.setAppopPrivileged(OP_MANAGE_IPSEC_TUNNELS, false);
|
||||||
|
|
||||||
sCM.unregisterNetworkCallback(sTunNetworkCallback);
|
sCM.unregisterNetworkCallback(sTunNetworkCallback);
|
||||||
|
|
||||||
@@ -169,50 +157,12 @@ public class IpSecManagerTunnelTest extends IpSecBaseTest {
|
|||||||
.dropShellPermissionIdentity();
|
.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
|
@Test
|
||||||
public void testSecurityExceptionCreateTunnelInterfaceWithoutAppop() throws Exception {
|
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
|
// 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
|
// Security exceptions are thrown regardless of IPv4/IPv6. Just test one
|
||||||
try {
|
try {
|
||||||
@@ -224,10 +174,10 @@ public class IpSecManagerTunnelTest extends IpSecBaseTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSecurityExceptionBuildTunnelTransformWithoutAppop() throws Exception {
|
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
|
// 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
|
// Security exceptions are thrown regardless of IPv4/IPv6. Just test one
|
||||||
try (IpSecManager.SecurityParameterIndex spi =
|
try (IpSecManager.SecurityParameterIndex spi =
|
||||||
@@ -253,19 +203,6 @@ public class IpSecManagerTunnelTest extends IpSecBaseTest {
|
|||||||
public abstract int run(Network ipsecNetwork) throws Exception;
|
public abstract int run(Network ipsecNetwork) throws Exception;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class TestNetworkCallback extends ConnectivityManager.NetworkCallback {
|
|
||||||
private final CompletableFuture<Network> 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(
|
private int getPacketSize(
|
||||||
int innerFamily, int outerFamily, boolean useEncap, boolean transportInTunnelMode) {
|
int innerFamily, int outerFamily, boolean useEncap, boolean transportInTunnelMode) {
|
||||||
int expectedPacketSize = TEST_DATA.length + UDP_HDRLEN;
|
int expectedPacketSize = TEST_DATA.length + UDP_HDRLEN;
|
||||||
@@ -499,8 +436,6 @@ public class IpSecManagerTunnelTest extends IpSecBaseTest {
|
|||||||
public void checkTunnelReflected(
|
public void checkTunnelReflected(
|
||||||
int innerFamily, int outerFamily, boolean useEncap, boolean transportInTunnelMode)
|
int innerFamily, int outerFamily, boolean useEncap, boolean transportInTunnelMode)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
if (!hasTunnelsFeature()) return;
|
|
||||||
|
|
||||||
InetAddress localInner = innerFamily == AF_INET ? LOCAL_INNER_4 : LOCAL_INNER_6;
|
InetAddress localInner = innerFamily == AF_INET ? LOCAL_INNER_4 : LOCAL_INNER_6;
|
||||||
InetAddress remoteInner = innerFamily == AF_INET ? REMOTE_INNER_4 : REMOTE_INNER_6;
|
InetAddress remoteInner = innerFamily == AF_INET ? REMOTE_INNER_4 : REMOTE_INNER_6;
|
||||||
|
|
||||||
@@ -580,7 +515,6 @@ public class IpSecManagerTunnelTest extends IpSecBaseTest {
|
|||||||
boolean transportInTunnelMode,
|
boolean transportInTunnelMode,
|
||||||
IpSecTunnelTestRunnableFactory factory)
|
IpSecTunnelTestRunnableFactory factory)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
if (!hasTunnelsFeature()) return;
|
|
||||||
|
|
||||||
InetAddress localInner = innerFamily == AF_INET ? LOCAL_INNER_4 : LOCAL_INNER_6;
|
InetAddress localInner = innerFamily == AF_INET ? LOCAL_INNER_4 : LOCAL_INNER_6;
|
||||||
InetAddress remoteInner = innerFamily == AF_INET ? REMOTE_INNER_4 : REMOTE_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)) {
|
mISM.createIpSecTunnelInterface(localOuter, remoteOuter, sTunNetwork)) {
|
||||||
// Build the test network
|
// Build the test network
|
||||||
tunnelIface.addAddress(localInner, innerPrefixLen);
|
tunnelIface.addAddress(localInner, innerPrefixLen);
|
||||||
testNetworkCb = setupAndGetTestNetwork(tunnelIface.getInterfaceName());
|
testNetworkCb = mCtsNetUtils.setupAndGetTestNetwork(tunnelIface.getInterfaceName());
|
||||||
Network testNetwork = testNetworkCb.getNetworkBlocking();
|
testNetworkCb.waitForAvailable();
|
||||||
|
Network testNetwork = testNetworkCb.currentNetwork;
|
||||||
|
|
||||||
// Check interface was created
|
// Check interface was created
|
||||||
assertNotNull(NetworkInterface.getByName(tunnelIface.getInterfaceName()));
|
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(
|
private EspHeader buildTransportModeEspPacket(
|
||||||
int spi, InetAddress src, InetAddress dst, int port, Payload payload) throws Exception {
|
int spi, InetAddress src, InetAddress dst, int port, Payload payload) throws Exception {
|
||||||
IpHeader preEspIpHeader = getIpHeader(payload.getProtocolId(), src, dst, payload);
|
IpHeader preEspIpHeader = getIpHeader(payload.getProtocolId(), src, dst, payload);
|
||||||
@@ -819,134 +742,158 @@ public class IpSecManagerTunnelTest extends IpSecBaseTest {
|
|||||||
// Transport-in-Tunnel mode tests
|
// Transport-in-Tunnel mode tests
|
||||||
@Test
|
@Test
|
||||||
public void testTransportInTunnelModeV4InV4() throws Exception {
|
public void testTransportInTunnelModeV4InV4() throws Exception {
|
||||||
|
assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
|
||||||
checkTunnelOutput(AF_INET, AF_INET, false, true);
|
checkTunnelOutput(AF_INET, AF_INET, false, true);
|
||||||
checkTunnelInput(AF_INET, AF_INET, false, true);
|
checkTunnelInput(AF_INET, AF_INET, false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTransportInTunnelModeV4InV4Reflected() throws Exception {
|
public void testTransportInTunnelModeV4InV4Reflected() throws Exception {
|
||||||
|
assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
|
||||||
checkTunnelReflected(AF_INET, AF_INET, false, true);
|
checkTunnelReflected(AF_INET, AF_INET, false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTransportInTunnelModeV4InV4UdpEncap() throws Exception {
|
public void testTransportInTunnelModeV4InV4UdpEncap() throws Exception {
|
||||||
|
assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
|
||||||
checkTunnelOutput(AF_INET, AF_INET, true, true);
|
checkTunnelOutput(AF_INET, AF_INET, true, true);
|
||||||
checkTunnelInput(AF_INET, AF_INET, true, true);
|
checkTunnelInput(AF_INET, AF_INET, true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTransportInTunnelModeV4InV4UdpEncapReflected() throws Exception {
|
public void testTransportInTunnelModeV4InV4UdpEncapReflected() throws Exception {
|
||||||
|
assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
|
||||||
checkTunnelReflected(AF_INET, AF_INET, false, true);
|
checkTunnelReflected(AF_INET, AF_INET, false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTransportInTunnelModeV4InV6() throws Exception {
|
public void testTransportInTunnelModeV4InV6() throws Exception {
|
||||||
|
assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
|
||||||
checkTunnelOutput(AF_INET, AF_INET6, false, true);
|
checkTunnelOutput(AF_INET, AF_INET6, false, true);
|
||||||
checkTunnelInput(AF_INET, AF_INET6, false, true);
|
checkTunnelInput(AF_INET, AF_INET6, false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTransportInTunnelModeV4InV6Reflected() throws Exception {
|
public void testTransportInTunnelModeV4InV6Reflected() throws Exception {
|
||||||
|
assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
|
||||||
checkTunnelReflected(AF_INET, AF_INET, false, true);
|
checkTunnelReflected(AF_INET, AF_INET, false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTransportInTunnelModeV6InV4() throws Exception {
|
public void testTransportInTunnelModeV6InV4() throws Exception {
|
||||||
|
assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
|
||||||
checkTunnelOutput(AF_INET6, AF_INET, false, true);
|
checkTunnelOutput(AF_INET6, AF_INET, false, true);
|
||||||
checkTunnelInput(AF_INET6, AF_INET, false, true);
|
checkTunnelInput(AF_INET6, AF_INET, false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTransportInTunnelModeV6InV4Reflected() throws Exception {
|
public void testTransportInTunnelModeV6InV4Reflected() throws Exception {
|
||||||
|
assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
|
||||||
checkTunnelReflected(AF_INET, AF_INET, false, true);
|
checkTunnelReflected(AF_INET, AF_INET, false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTransportInTunnelModeV6InV4UdpEncap() throws Exception {
|
public void testTransportInTunnelModeV6InV4UdpEncap() throws Exception {
|
||||||
|
assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
|
||||||
checkTunnelOutput(AF_INET6, AF_INET, true, true);
|
checkTunnelOutput(AF_INET6, AF_INET, true, true);
|
||||||
checkTunnelInput(AF_INET6, AF_INET, true, true);
|
checkTunnelInput(AF_INET6, AF_INET, true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTransportInTunnelModeV6InV4UdpEncapReflected() throws Exception {
|
public void testTransportInTunnelModeV6InV4UdpEncapReflected() throws Exception {
|
||||||
|
assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
|
||||||
checkTunnelReflected(AF_INET, AF_INET, false, true);
|
checkTunnelReflected(AF_INET, AF_INET, false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTransportInTunnelModeV6InV6() throws Exception {
|
public void testTransportInTunnelModeV6InV6() throws Exception {
|
||||||
|
assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
|
||||||
checkTunnelOutput(AF_INET, AF_INET6, false, true);
|
checkTunnelOutput(AF_INET, AF_INET6, false, true);
|
||||||
checkTunnelInput(AF_INET, AF_INET6, false, true);
|
checkTunnelInput(AF_INET, AF_INET6, false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTransportInTunnelModeV6InV6Reflected() throws Exception {
|
public void testTransportInTunnelModeV6InV6Reflected() throws Exception {
|
||||||
|
assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
|
||||||
checkTunnelReflected(AF_INET, AF_INET, false, true);
|
checkTunnelReflected(AF_INET, AF_INET, false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tunnel mode tests
|
// Tunnel mode tests
|
||||||
@Test
|
@Test
|
||||||
public void testTunnelV4InV4() throws Exception {
|
public void testTunnelV4InV4() throws Exception {
|
||||||
|
assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
|
||||||
checkTunnelOutput(AF_INET, AF_INET, false, false);
|
checkTunnelOutput(AF_INET, AF_INET, false, false);
|
||||||
checkTunnelInput(AF_INET, AF_INET, false, false);
|
checkTunnelInput(AF_INET, AF_INET, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTunnelV4InV4Reflected() throws Exception {
|
public void testTunnelV4InV4Reflected() throws Exception {
|
||||||
|
assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
|
||||||
checkTunnelReflected(AF_INET, AF_INET, false, false);
|
checkTunnelReflected(AF_INET, AF_INET, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTunnelV4InV4UdpEncap() throws Exception {
|
public void testTunnelV4InV4UdpEncap() throws Exception {
|
||||||
|
assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
|
||||||
checkTunnelOutput(AF_INET, AF_INET, true, false);
|
checkTunnelOutput(AF_INET, AF_INET, true, false);
|
||||||
checkTunnelInput(AF_INET, AF_INET, true, false);
|
checkTunnelInput(AF_INET, AF_INET, true, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTunnelV4InV4UdpEncapReflected() throws Exception {
|
public void testTunnelV4InV4UdpEncapReflected() throws Exception {
|
||||||
|
assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
|
||||||
checkTunnelReflected(AF_INET, AF_INET, true, false);
|
checkTunnelReflected(AF_INET, AF_INET, true, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTunnelV4InV6() throws Exception {
|
public void testTunnelV4InV6() throws Exception {
|
||||||
|
assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
|
||||||
checkTunnelOutput(AF_INET, AF_INET6, false, false);
|
checkTunnelOutput(AF_INET, AF_INET6, false, false);
|
||||||
checkTunnelInput(AF_INET, AF_INET6, false, false);
|
checkTunnelInput(AF_INET, AF_INET6, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTunnelV4InV6Reflected() throws Exception {
|
public void testTunnelV4InV6Reflected() throws Exception {
|
||||||
|
assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
|
||||||
checkTunnelReflected(AF_INET, AF_INET6, false, false);
|
checkTunnelReflected(AF_INET, AF_INET6, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTunnelV6InV4() throws Exception {
|
public void testTunnelV6InV4() throws Exception {
|
||||||
|
assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
|
||||||
checkTunnelOutput(AF_INET6, AF_INET, false, false);
|
checkTunnelOutput(AF_INET6, AF_INET, false, false);
|
||||||
checkTunnelInput(AF_INET6, AF_INET, false, false);
|
checkTunnelInput(AF_INET6, AF_INET, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTunnelV6InV4Reflected() throws Exception {
|
public void testTunnelV6InV4Reflected() throws Exception {
|
||||||
|
assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
|
||||||
checkTunnelReflected(AF_INET6, AF_INET, false, false);
|
checkTunnelReflected(AF_INET6, AF_INET, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTunnelV6InV4UdpEncap() throws Exception {
|
public void testTunnelV6InV4UdpEncap() throws Exception {
|
||||||
|
assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
|
||||||
checkTunnelOutput(AF_INET6, AF_INET, true, false);
|
checkTunnelOutput(AF_INET6, AF_INET, true, false);
|
||||||
checkTunnelInput(AF_INET6, AF_INET, true, false);
|
checkTunnelInput(AF_INET6, AF_INET, true, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTunnelV6InV4UdpEncapReflected() throws Exception {
|
public void testTunnelV6InV4UdpEncapReflected() throws Exception {
|
||||||
|
assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
|
||||||
checkTunnelReflected(AF_INET6, AF_INET, true, false);
|
checkTunnelReflected(AF_INET6, AF_INET, true, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTunnelV6InV6() throws Exception {
|
public void testTunnelV6InV6() throws Exception {
|
||||||
|
assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
|
||||||
checkTunnelOutput(AF_INET6, AF_INET6, false, false);
|
checkTunnelOutput(AF_INET6, AF_INET6, false, false);
|
||||||
checkTunnelInput(AF_INET6, AF_INET6, false, false);
|
checkTunnelInput(AF_INET6, AF_INET6, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTunnelV6InV6Reflected() throws Exception {
|
public void testTunnelV6InV6Reflected() throws Exception {
|
||||||
|
assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
|
||||||
checkTunnelReflected(AF_INET6, AF_INET6, false, false);
|
checkTunnelReflected(AF_INET6, AF_INET6, false, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import java.nio.ShortBuffer;
|
|||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
import javax.crypto.Mac;
|
import javax.crypto.Mac;
|
||||||
import javax.crypto.spec.IvParameterSpec;
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
@@ -443,6 +444,19 @@ public class PacketUtils {
|
|||||||
return Arrays.copyOfRange(buffer.array(), 0, buffer.position());
|
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
|
* Debug printing
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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.IPPROTO_ESP;
|
||||||
import static android.net.cts.PacketUtils.UDP_HDRLEN;
|
import static android.net.cts.PacketUtils.UDP_HDRLEN;
|
||||||
import static android.system.OsConstants.IPPROTO_UDP;
|
import static android.system.OsConstants.IPPROTO_UDP;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
@@ -39,19 +39,18 @@ import java.util.function.Predicate;
|
|||||||
public class TunUtils {
|
public class TunUtils {
|
||||||
private static final String TAG = TunUtils.class.getSimpleName();
|
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 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<byte[]> mPackets = new ArrayList<>();
|
private final List<byte[]> mPackets = new ArrayList<>();
|
||||||
|
private final ParcelFileDescriptor mTunFd;
|
||||||
private final Thread mReaderThread;
|
private final Thread mReaderThread;
|
||||||
|
|
||||||
public TunUtils(ParcelFileDescriptor tunFd) {
|
public TunUtils(ParcelFileDescriptor tunFd) {
|
||||||
@@ -112,46 +111,15 @@ public class TunUtils {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
protected byte[] awaitPacket(Predicate<byte[]> verifier) throws Exception {
|
||||||
* Checks if the specified bytes were ever sent in plaintext.
|
|
||||||
*
|
|
||||||
* <p>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<byte[]> 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 {
|
|
||||||
long endTime = System.currentTimeMillis() + TIMEOUT;
|
long endTime = System.currentTimeMillis() + TIMEOUT;
|
||||||
int startIndex = 0;
|
int startIndex = 0;
|
||||||
|
|
||||||
synchronized (mPackets) {
|
synchronized (mPackets) {
|
||||||
while (System.currentTimeMillis() < endTime) {
|
while (System.currentTimeMillis() < endTime) {
|
||||||
byte[] espPkt = getEspPacket(spi, useEncap, startIndex);
|
final byte[] pkt = getFirstMatchingPacket(verifier, startIndex);
|
||||||
if (espPkt != null) {
|
if (pkt != null) {
|
||||||
// Validate packet size
|
return pkt; // We've found the packet we're looking for.
|
||||||
assertEquals(expectedPacketSize, espPkt.length);
|
|
||||||
|
|
||||||
// Always check plaintext from start
|
|
||||||
assertFalse(hasPlaintextPacket(plaintext, 0));
|
|
||||||
return espPkt; // We've found the packet we're looking for.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
startIndex = mPackets.size();
|
startIndex = mPackets.size();
|
||||||
@@ -162,10 +130,21 @@ public class TunUtils {
|
|||||||
mPackets.wait(waitTimeout);
|
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) {
|
private static boolean isSpiEqual(byte[] pkt, int espOffset, int spi) {
|
||||||
@@ -176,6 +155,24 @@ public class TunUtils {
|
|||||||
&& pkt[espOffset + 3] == (byte) (spi & 0xff);
|
&& 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) {
|
private static boolean isEsp(byte[] pkt, int spi, boolean encap) {
|
||||||
if (isIpv6(pkt)) {
|
if (isIpv6(pkt)) {
|
||||||
// IPv6 UDP encap not supported by kernels; assume non-encap.
|
// 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
|
// First nibble shows IP version. 0x60 for IPv6
|
||||||
return (pkt[0] & (byte) 0xF0) == (byte) 0x60;
|
return (pkt[0] & (byte) 0xF0) == (byte) 0x60;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import static android.Manifest.permission.NETWORK_SETTINGS;
|
|||||||
import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
|
import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
|
||||||
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
|
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
|
||||||
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
|
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.assertEquals;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
@@ -27,11 +28,13 @@ import static org.junit.Assert.assertTrue;
|
|||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
import android.annotation.NonNull;
|
import android.annotation.NonNull;
|
||||||
|
import android.app.AppOpsManager;
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
import android.net.ConnectivityManager.NetworkCallback;
|
import android.net.ConnectivityManager.NetworkCallback;
|
||||||
import android.net.LinkProperties;
|
import android.net.LinkProperties;
|
||||||
@@ -40,7 +43,12 @@ import android.net.NetworkCapabilities;
|
|||||||
import android.net.NetworkInfo;
|
import android.net.NetworkInfo;
|
||||||
import android.net.NetworkInfo.State;
|
import android.net.NetworkInfo.State;
|
||||||
import android.net.NetworkRequest;
|
import android.net.NetworkRequest;
|
||||||
|
import android.net.TestNetworkManager;
|
||||||
import android.net.wifi.WifiManager;
|
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.provider.Settings;
|
||||||
import android.system.Os;
|
import android.system.Os;
|
||||||
import android.system.OsConstants;
|
import android.system.OsConstants;
|
||||||
@@ -73,6 +81,7 @@ public final class CtsNetUtils {
|
|||||||
public static final String NETWORK_CALLBACK_ACTION =
|
public static final String NETWORK_CALLBACK_ACTION =
|
||||||
"ConnectivityManagerTest.NetworkCallbackAction";
|
"ConnectivityManagerTest.NetworkCallbackAction";
|
||||||
|
|
||||||
|
private final IBinder mBinder = new Binder();
|
||||||
private final Context mContext;
|
private final Context mContext;
|
||||||
private final ConnectivityManager mCm;
|
private final ConnectivityManager mCm;
|
||||||
private final ContentResolver mCR;
|
private final ContentResolver mCR;
|
||||||
@@ -88,6 +97,51 @@ public final class CtsNetUtils {
|
|||||||
mCR = context.getContentResolver();
|
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
|
||||||
|
*
|
||||||
|
* <p>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
|
// Toggle WiFi twice, leaving it in the state it started in
|
||||||
public void toggleWifi() {
|
public void toggleWifi() {
|
||||||
if (mWifiManager.isWifiEnabled()) {
|
if (mWifiManager.isWifiEnabled()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user