Add cts test for VPN incoming packets blocking
Bug: 206482423 Test: atest HostsideVpnTests#testBlockIncomingPackets --iterations 30 Change-Id: I5a9c515c163f0f19e261fe3468e246ae299fa261
This commit is contained in:
parent
a530228ecd
commit
2e84a63561
5 changed files with 224 additions and 5 deletions
|
|
@ -20,6 +20,7 @@ import android.os.ParcelFileDescriptor;
|
|||
|
||||
interface IRemoteSocketFactory {
|
||||
ParcelFileDescriptor openSocketFd(String host, int port, int timeoutMs);
|
||||
ParcelFileDescriptor openDatagramSocketFd();
|
||||
String getPackageName();
|
||||
int getUid();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,9 +83,19 @@ public class RemoteSocketFactoryClient {
|
|||
public FileDescriptor openSocketFd(String host, int port, int timeoutMs)
|
||||
throws RemoteException, ErrnoException, IOException {
|
||||
// Dup the filedescriptor so ParcelFileDescriptor's finalizer doesn't garbage collect it
|
||||
// and cause our fd to become invalid. http://b/35927643 .
|
||||
ParcelFileDescriptor pfd = mService.openSocketFd(host, port, timeoutMs);
|
||||
FileDescriptor fd = Os.dup(pfd.getFileDescriptor());
|
||||
// and cause fd to become invalid. http://b/35927643.
|
||||
final ParcelFileDescriptor pfd = mService.openSocketFd(host, port, timeoutMs);
|
||||
final FileDescriptor fd = Os.dup(pfd.getFileDescriptor());
|
||||
pfd.close();
|
||||
return fd;
|
||||
}
|
||||
|
||||
public FileDescriptor openDatagramSocketFd()
|
||||
throws RemoteException, ErrnoException, IOException {
|
||||
// Dup the filedescriptor so ParcelFileDescriptor's finalizer doesn't garbage collect it
|
||||
// and cause fd to become invalid. http://b/35927643.
|
||||
final ParcelFileDescriptor pfd = mService.openDatagramSocketFd();
|
||||
final FileDescriptor fd = Os.dup(pfd.getFileDescriptor());
|
||||
pfd.close();
|
||||
return fd;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package com.android.cts.net.hostside;
|
||||
|
||||
import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
|
||||
import static android.Manifest.permission.NETWORK_SETTINGS;
|
||||
import static android.content.pm.PackageManager.FEATURE_TELEPHONY;
|
||||
import static android.content.pm.PackageManager.FEATURE_WIFI;
|
||||
|
|
@ -35,6 +36,8 @@ import static android.test.MoreAsserts.assertNotEqual;
|
|||
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
|
||||
|
||||
import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
|
||||
import static com.android.networkstack.apishim.ConstantsShim.BLOCKED_REASON_LOCKDOWN_VPN;
|
||||
import static com.android.networkstack.apishim.ConstantsShim.BLOCKED_REASON_NONE;
|
||||
import static com.android.testutils.Cleanup.testAndCleanup;
|
||||
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
|
||||
|
||||
|
|
@ -59,12 +62,15 @@ import android.content.pm.PackageManager;
|
|||
import android.database.Cursor;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.ConnectivityManager.NetworkCallback;
|
||||
import android.net.LinkAddress;
|
||||
import android.net.LinkProperties;
|
||||
import android.net.Network;
|
||||
import android.net.NetworkCapabilities;
|
||||
import android.net.NetworkRequest;
|
||||
import android.net.Proxy;
|
||||
import android.net.ProxyInfo;
|
||||
import android.net.TestNetworkInterface;
|
||||
import android.net.TestNetworkManager;
|
||||
import android.net.TransportInfo;
|
||||
import android.net.Uri;
|
||||
import android.net.VpnManager;
|
||||
|
|
@ -72,6 +78,7 @@ import android.net.VpnService;
|
|||
import android.net.VpnTransportInfo;
|
||||
import android.net.cts.util.CtsNetUtils;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
|
@ -90,11 +97,13 @@ import android.telephony.TelephonyManager;
|
|||
import android.test.MoreAsserts;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.Range;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import com.android.compatibility.common.util.BlockingBroadcastReceiver;
|
||||
import com.android.modules.utils.build.SdkLevel;
|
||||
import com.android.net.module.util.PacketBuilder;
|
||||
import com.android.testutils.DevSdkIgnoreRule;
|
||||
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
|
||||
import com.android.testutils.RecorderCallback;
|
||||
|
|
@ -113,12 +122,14 @@ import java.io.InputStream;
|
|||
import java.io.OutputStream;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
|
@ -164,6 +175,12 @@ public class VpnTest {
|
|||
private static final String PRIVATE_DNS_SPECIFIER_SETTING = "private_dns_specifier";
|
||||
private static final int NETWORK_CALLBACK_TIMEOUT_MS = 30_000;
|
||||
|
||||
private static final LinkAddress TEST_IP4_DST_ADDR = new LinkAddress("198.51.100.1/24");
|
||||
private static final LinkAddress TEST_IP4_SRC_ADDR = new LinkAddress("198.51.100.2/24");
|
||||
private static final LinkAddress TEST_IP6_DST_ADDR = new LinkAddress("2001:db8:1:3::1/64");
|
||||
private static final LinkAddress TEST_IP6_SRC_ADDR = new LinkAddress("2001:db8:1:3::2/64");
|
||||
private static final short TEST_SRC_PORT = 5555;
|
||||
|
||||
public static String TAG = "VpnTest";
|
||||
public static int TIMEOUT_MS = 3 * 1000;
|
||||
public static int SOCKET_TIMEOUT_MS = 100;
|
||||
|
|
@ -1572,4 +1589,180 @@ public class VpnTest {
|
|||
return future.get(timeout, unit);
|
||||
}
|
||||
}
|
||||
|
||||
private static final boolean EXPECT_PASS = false;
|
||||
private static final boolean EXPECT_BLOCK = true;
|
||||
|
||||
@Test @IgnoreUpTo(Build.VERSION_CODES.R)
|
||||
public void testBlockIncomingPackets() throws Exception {
|
||||
assumeTrue(supportedHardware());
|
||||
final Network network = mCM.getActiveNetwork();
|
||||
assertNotNull("Requires a working Internet connection", network);
|
||||
|
||||
final int remoteUid = mRemoteSocketFactoryClient.getUid();
|
||||
final List<Range<Integer>> lockdownRange = List.of(new Range<>(remoteUid, remoteUid));
|
||||
final DetailedBlockedStatusCallback remoteUidCallback = new DetailedBlockedStatusCallback();
|
||||
|
||||
// Create a TUN interface
|
||||
final FileDescriptor tunFd = runWithShellPermissionIdentity(() -> {
|
||||
final TestNetworkManager tnm = getInstrumentation().getContext().getSystemService(
|
||||
TestNetworkManager.class);
|
||||
final TestNetworkInterface iface = tnm.createTunInterface(List.of(
|
||||
TEST_IP4_DST_ADDR, TEST_IP6_DST_ADDR));
|
||||
return iface.getFileDescriptor().getFileDescriptor();
|
||||
}, MANAGE_TEST_NETWORKS);
|
||||
|
||||
// Create a remote UDP socket
|
||||
final FileDescriptor remoteUdpFd = mRemoteSocketFactoryClient.openDatagramSocketFd();
|
||||
|
||||
testAndCleanup(() -> {
|
||||
runWithShellPermissionIdentity(() -> {
|
||||
mCM.registerDefaultNetworkCallbackForUid(remoteUid, remoteUidCallback,
|
||||
new Handler(Looper.getMainLooper()));
|
||||
}, NETWORK_SETTINGS);
|
||||
remoteUidCallback.expectAvailableCallbacks(network);
|
||||
|
||||
// The remote UDP socket can receive packets coming from the TUN interface
|
||||
checkBlockIncomingPacket(tunFd, remoteUdpFd, EXPECT_PASS);
|
||||
|
||||
// Lockdown uid that has the remote UDP socket
|
||||
runWithShellPermissionIdentity(() -> {
|
||||
mCM.setRequireVpnForUids(true /* requireVpn */, lockdownRange);
|
||||
}, NETWORK_SETTINGS);
|
||||
|
||||
// setRequireVpnForUids setup a lockdown rule asynchronously. So it needs to wait for
|
||||
// BlockedStatusCallback to be fired before checking the blocking status of incoming
|
||||
// packets.
|
||||
remoteUidCallback.expectBlockedStatusCallback(network, BLOCKED_REASON_LOCKDOWN_VPN);
|
||||
|
||||
if (SdkLevel.isAtLeastT()) {
|
||||
// On T and above, lockdown rule drop packets not coming from lo regardless of the
|
||||
// VPN connectivity.
|
||||
checkBlockIncomingPacket(tunFd, remoteUdpFd, EXPECT_BLOCK);
|
||||
}
|
||||
|
||||
// Start the VPN that has default routes. This VPN should have interface filtering rule
|
||||
// for incoming packet and drop packets not coming from lo nor the VPN interface.
|
||||
final String allowedApps =
|
||||
mRemoteSocketFactoryClient.getPackageName() + "," + mPackageName;
|
||||
startVpn(new String[]{"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
|
||||
new String[]{"0.0.0.0/0", "::/0"}, allowedApps, "" /* disallowedApplications */,
|
||||
null /* proxyInfo */, null /* underlyingNetworks */,
|
||||
false /* isAlwaysMetered */);
|
||||
|
||||
checkBlockIncomingPacket(tunFd, remoteUdpFd, EXPECT_BLOCK);
|
||||
}, /* cleanup */ () -> {
|
||||
mCM.unregisterNetworkCallback(remoteUidCallback);
|
||||
}, /* cleanup */ () -> {
|
||||
Os.close(tunFd);
|
||||
}, /* cleanup */ () -> {
|
||||
Os.close(remoteUdpFd);
|
||||
}, /* cleanup */ () -> {
|
||||
runWithShellPermissionIdentity(() -> {
|
||||
mCM.setRequireVpnForUids(false /* requireVpn */, lockdownRange);
|
||||
}, NETWORK_SETTINGS);
|
||||
});
|
||||
}
|
||||
|
||||
private ByteBuffer buildIpv4UdpPacket(final Inet4Address dstAddr, final Inet4Address srcAddr,
|
||||
final short dstPort, final short srcPort, final byte[] payload) throws IOException {
|
||||
|
||||
final ByteBuffer buffer = PacketBuilder.allocate(false /* hasEther */,
|
||||
OsConstants.IPPROTO_IP, OsConstants.IPPROTO_UDP, payload.length);
|
||||
final PacketBuilder packetBuilder = new PacketBuilder(buffer);
|
||||
|
||||
packetBuilder.writeIpv4Header(
|
||||
(byte) 0 /* TOS */,
|
||||
(short) 27149 /* ID */,
|
||||
(short) 0x4000 /* flags=DF, offset=0 */,
|
||||
(byte) 64 /* TTL */,
|
||||
(byte) OsConstants.IPPROTO_UDP,
|
||||
srcAddr,
|
||||
dstAddr);
|
||||
packetBuilder.writeUdpHeader(srcPort, dstPort);
|
||||
buffer.put(payload);
|
||||
|
||||
return packetBuilder.finalizePacket();
|
||||
}
|
||||
|
||||
private ByteBuffer buildIpv6UdpPacket(final Inet6Address dstAddr, final Inet6Address srcAddr,
|
||||
final short dstPort, final short srcPort, final byte[] payload) throws IOException {
|
||||
|
||||
final ByteBuffer buffer = PacketBuilder.allocate(false /* hasEther */,
|
||||
OsConstants.IPPROTO_IPV6, OsConstants.IPPROTO_UDP, payload.length);
|
||||
final PacketBuilder packetBuilder = new PacketBuilder(buffer);
|
||||
|
||||
packetBuilder.writeIpv6Header(
|
||||
0x60000000 /* version=6, traffic class=0, flow label=0 */,
|
||||
(byte) OsConstants.IPPROTO_UDP,
|
||||
(short) 64 /* hop limit */,
|
||||
srcAddr,
|
||||
dstAddr);
|
||||
packetBuilder.writeUdpHeader(srcPort, dstPort);
|
||||
buffer.put(payload);
|
||||
|
||||
return packetBuilder.finalizePacket();
|
||||
}
|
||||
|
||||
private void checkBlockUdp(
|
||||
final FileDescriptor srcTunFd,
|
||||
final FileDescriptor dstUdpFd,
|
||||
final boolean ipv6,
|
||||
final boolean expectBlock) throws Exception {
|
||||
final Random random = new Random();
|
||||
final byte[] sendData = new byte[100];
|
||||
random.nextBytes(sendData);
|
||||
final short dstPort = (short) ((InetSocketAddress) Os.getsockname(dstUdpFd)).getPort();
|
||||
|
||||
ByteBuffer buf;
|
||||
if (ipv6) {
|
||||
buf = buildIpv6UdpPacket(
|
||||
(Inet6Address) TEST_IP6_DST_ADDR.getAddress(),
|
||||
(Inet6Address) TEST_IP6_SRC_ADDR.getAddress(),
|
||||
dstPort, TEST_SRC_PORT, sendData);
|
||||
} else {
|
||||
buf = buildIpv4UdpPacket(
|
||||
(Inet4Address) TEST_IP4_DST_ADDR.getAddress(),
|
||||
(Inet4Address) TEST_IP4_SRC_ADDR.getAddress(),
|
||||
dstPort, TEST_SRC_PORT, sendData);
|
||||
}
|
||||
|
||||
Os.write(srcTunFd, buf);
|
||||
|
||||
final StructPollfd pollfd = new StructPollfd();
|
||||
pollfd.events = (short) POLLIN;
|
||||
pollfd.fd = dstUdpFd;
|
||||
final int ret = Os.poll(new StructPollfd[]{pollfd}, SOCKET_TIMEOUT_MS);
|
||||
|
||||
if (expectBlock) {
|
||||
assertEquals("Expect not to receive a packet but received a packet", 0, ret);
|
||||
} else {
|
||||
assertEquals("Expect to receive a packet but did not receive a packet", 1, ret);
|
||||
final byte[] recvData = new byte[sendData.length];
|
||||
final int readSize = Os.read(dstUdpFd, recvData, 0 /* byteOffset */, recvData.length);
|
||||
assertEquals(recvData.length, readSize);
|
||||
MoreAsserts.assertEquals(sendData, recvData);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkBlockIncomingPacket(
|
||||
final FileDescriptor srcTunFd,
|
||||
final FileDescriptor dstUdpFd,
|
||||
final boolean expectBlock) throws Exception {
|
||||
checkBlockUdp(srcTunFd, dstUdpFd, false /* ipv6 */, expectBlock);
|
||||
checkBlockUdp(srcTunFd, dstUdpFd, true /* ipv6 */, expectBlock);
|
||||
}
|
||||
|
||||
private class DetailedBlockedStatusCallback extends TestableNetworkCallback {
|
||||
public void expectAvailableCallbacks(Network network) {
|
||||
super.expectAvailableCallbacks(network, false /* suspended */, true /* validated */,
|
||||
BLOCKED_REASON_NONE, NETWORK_CALLBACK_TIMEOUT_MS);
|
||||
}
|
||||
public void expectBlockedStatusCallback(Network network, int blockedStatus) {
|
||||
super.expectBlockedStatusCallback(blockedStatus, network, NETWORK_CALLBACK_TIMEOUT_MS);
|
||||
}
|
||||
public void onBlockedStatusChanged(Network network, int blockedReasons) {
|
||||
getHistory().add(new CallbackEntry.BlockedStatusInt(network, blockedReasons));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,16 +17,17 @@
|
|||
package com.android.cts.net.hostside.app2;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.Process;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.cts.net.hostside.IRemoteSocketFactory;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
|
||||
|
||||
public class RemoteSocketFactoryService extends Service {
|
||||
|
|
@ -54,6 +55,16 @@ public class RemoteSocketFactoryService extends Service {
|
|||
public int getUid() {
|
||||
return Process.myUid();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor openDatagramSocketFd() {
|
||||
try {
|
||||
final DatagramSocket s = new DatagramSocket();
|
||||
return ParcelFileDescriptor.fromDatagramSocket(s);
|
||||
} catch (SocketException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -116,4 +116,8 @@ public class HostsideVpnTests extends HostsideNetworkTestCase {
|
|||
public void testInterleavedRoutes() throws Exception {
|
||||
runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testInterleavedRoutes");
|
||||
}
|
||||
|
||||
public void testBlockIncomingPackets() throws Exception {
|
||||
runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testBlockIncomingPackets");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue