Merge "Add cts test for VPN incoming packets blocking"
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 68176ad..6986e7e 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 @@
 
 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 80f99b6..01fbd66 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 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 dd8b523..5f032be 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 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.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.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.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.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 @@
     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 @@
             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));
+        }
+    }
 }
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 b1b7d77..fb6d16f 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 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 3821f87..4d90a4a 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 void testInterleavedRoutes() throws Exception {
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testInterleavedRoutes");
     }
+
+    public void testBlockIncomingPackets() throws Exception {
+        runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testBlockIncomingPackets");
+    }
 }