From 2e84a63561ffbc107e5e495642d111641e9984d2 Mon Sep 17 00:00:00 2001 From: Motomu Utsumi Date: Tue, 24 May 2022 02:02:11 +0000 Subject: [PATCH] Add cts test for VPN incoming packets blocking Bug: 206482423 Test: atest HostsideVpnTests#testBlockIncomingPackets --iterations 30 Change-Id: I5a9c515c163f0f19e261fe3468e246ae299fa261 --- .../net/hostside/IRemoteSocketFactory.aidl | 1 + .../hostside/RemoteSocketFactoryClient.java | 16 +- .../com/android/cts/net/hostside/VpnTest.java | 193 ++++++++++++++++++ .../app2/RemoteSocketFactoryService.java | 15 +- .../com/android/cts/net/HostsideVpnTests.java | 4 + 5 files changed, 224 insertions(+), 5 deletions(-) diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/IRemoteSocketFactory.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IRemoteSocketFactory.aidl index 68176ad80d..6986e7e4df 100644 --- a/tests/cts/hostside/aidl/com/android/cts/net/hostside/IRemoteSocketFactory.aidl +++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IRemoteSocketFactory.aidl @@ -20,6 +20,7 @@ import android.os.ParcelFileDescriptor; interface IRemoteSocketFactory { ParcelFileDescriptor openSocketFd(String host, int port, int timeoutMs); + ParcelFileDescriptor openDatagramSocketFd(); String getPackageName(); int getUid(); } diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/RemoteSocketFactoryClient.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RemoteSocketFactoryClient.java index 80f99b6605..01fbd6692a 100644 --- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/RemoteSocketFactoryClient.java +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RemoteSocketFactoryClient.java @@ -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; } diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java index dd8b523e4a..5f032be050 100755 --- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java @@ -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> 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)); + } + } } diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/RemoteSocketFactoryService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/RemoteSocketFactoryService.java index b1b7d77ae1..fb6d16f3ea 100644 --- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/RemoteSocketFactoryService.java +++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/RemoteSocketFactoryService.java @@ -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 diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java index 3821f87a0e..4d90a4a4d2 100644 --- a/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java +++ b/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java @@ -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"); + } }