Test TCP and ICMPv6 on VPNs in addition to UDP.

Bug: 15605143
Change-Id: Ifd7a646990619057e714a789902df2c157a768c0
diff --git a/hostsidetests/net/app/src/com/android/cts/net/hostside/MyVpnService.java b/hostsidetests/net/app/src/com/android/cts/net/hostside/MyVpnService.java
index 1a12aaa..a3f400c 100644
--- a/hostsidetests/net/app/src/com/android/cts/net/hostside/MyVpnService.java
+++ b/hostsidetests/net/app/src/com/android/cts/net/hostside/MyVpnService.java
@@ -34,7 +34,7 @@
     private static int MTU = 1799;
 
     private ParcelFileDescriptor mFd = null;
-    private UdpReflector mUdpReflector = null;
+    private PacketReflector mPacketReflector = null;
 
     @Override
     public int onStartCommand(Intent intent, int flags, int startId) {
@@ -127,14 +127,14 @@
         mFd = builder.establish();
         Log.i(TAG, "Established, fd=" + (mFd == null ? "null" : mFd.getFd()));
 
-        mUdpReflector = new UdpReflector(mFd.getFileDescriptor(), MTU);
-        mUdpReflector.start();
+        mPacketReflector = new PacketReflector(mFd.getFileDescriptor(), MTU);
+        mPacketReflector.start();
     }
 
     private void stop() {
-        if (mUdpReflector != null) {
-            mUdpReflector.interrupt();
-            mUdpReflector = null;
+        if (mPacketReflector != null) {
+            mPacketReflector.interrupt();
+            mPacketReflector = null;
         }
         try {
             if (mFd != null) {
diff --git a/hostsidetests/net/app/src/com/android/cts/net/hostside/PacketReflector.java b/hostsidetests/net/app/src/com/android/cts/net/hostside/PacketReflector.java
new file mode 100644
index 0000000..dd0f792
--- /dev/null
+++ b/hostsidetests/net/app/src/com/android/cts/net/hostside/PacketReflector.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2014 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 com.android.cts.net.hostside;
+
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+public class PacketReflector extends Thread {
+
+    private static int IPV4_HEADER_LENGTH = 20;
+    private static int IPV6_HEADER_LENGTH = 40;
+
+    private static int IPV4_ADDR_OFFSET = 12;
+    private static int IPV6_ADDR_OFFSET = 8;
+    private static int IPV4_ADDR_LENGTH = 4;
+    private static int IPV6_ADDR_LENGTH = 16;
+
+    private static int IPV4_PROTO_OFFSET = 9;
+    private static int IPV6_PROTO_OFFSET = 6;
+
+    private static final byte IPPROTO_ICMP = 1;
+    private static final byte IPPROTO_TCP = 6;
+    private static final byte IPPROTO_UDP = 17;
+    private static final byte IPPROTO_ICMPV6 = 58;
+
+    private static int ICMP_HEADER_LENGTH = 8;
+    private static int TCP_HEADER_LENGTH = 20;
+    private static int UDP_HEADER_LENGTH = 8;
+
+    private static final byte ICMP_ECHO = 8;
+    private static final byte ICMP_ECHOREPLY = 0;
+    private static final byte ICMPV6_ECHO_REQUEST = (byte) 128;
+    private static final byte ICMPV6_ECHO_REPLY = (byte) 129;
+
+    private static String TAG = "PacketReflector";
+
+    private FileDescriptor mFd;
+    private byte[] mBuf;
+
+    public PacketReflector(FileDescriptor fd, int mtu) {
+        super("PacketReflector");
+        mFd = fd;
+        mBuf = new byte[mtu];
+    }
+
+    private static void swapBytes(byte[] buf, int pos1, int pos2, int len) {
+        for (int i = 0; i < len; i++) {
+            byte b = buf[pos1 + i];
+            buf[pos1 + i] = buf[pos2 + i];
+            buf[pos2 + i] = b;
+        }
+    }
+
+    private static void swapAddresses(byte[] buf, int version) {
+        int addrPos, addrLen;
+        switch(version) {
+            case 4:
+                addrPos = IPV4_ADDR_OFFSET;
+                addrLen = IPV4_ADDR_LENGTH;
+                break;
+            case 6:
+                addrPos = IPV6_ADDR_OFFSET;
+                addrLen = IPV6_ADDR_LENGTH;
+                break;
+            default:
+                throw new IllegalArgumentException();
+        }
+        swapBytes(buf, addrPos, addrPos + addrLen, addrLen);
+    }
+
+    // Reflect TCP packets: swap the source and destination addresses, but don't change the ports.
+    // This is used by the test to "connect to itself" through the VPN.
+    private void processTcpPacket(byte[] buf, int version, int len, int hdrLen) {
+        if (len < hdrLen + TCP_HEADER_LENGTH) {
+            return;
+        }
+
+        // Swap src and dst IP addresses.
+        swapAddresses(buf, version);
+
+        // Send the packet back.
+        writePacket(buf, len);
+    }
+
+    // Echo UDP packets: swap source and destination addresses, and source and destination ports.
+    // This is used by the test to check that the bytes it sends are echoed back.
+    private void processUdpPacket(byte[] buf, int version, int len, int hdrLen) {
+        if (len < hdrLen + UDP_HEADER_LENGTH) {
+            return;
+        }
+
+        // Swap src and dst IP addresses.
+        swapAddresses(buf, version);
+
+        // Swap dst and src ports.
+        int portOffset = hdrLen;
+        swapBytes(buf, portOffset, portOffset + 2, 2);
+
+        // Send the packet back.
+        writePacket(buf, len);
+    }
+
+    private void processIcmpPacket(byte[] buf, int version, int len, int hdrLen) {
+        if (len < hdrLen + ICMP_HEADER_LENGTH) {
+            return;
+        }
+
+        byte type = buf[hdrLen];
+        if (!(version == 4 && type == ICMP_ECHO) &&
+            !(version == 6 && type == ICMPV6_ECHO_REQUEST)) {
+            return;
+        }
+
+        // Save the ping packet we received.
+        byte[] request = buf.clone();
+
+        // Swap src and dst IP addresses, and send the packet back.
+        // This effectively pings the device to see if it replies.
+        swapAddresses(buf, version);
+        writePacket(buf, len);
+
+        // The device should have replied, and buf should now contain a ping response.
+        int received = readPacket(buf);
+        if (received != len) {
+            Log.i(TAG, "Reflecting ping did not result in ping response: " +
+                       "read=" + received + " expected=" + len);
+            return;
+        }
+
+        // Compare the response we got with the original packet.
+        // The only thing that should have changed are addresses, type and checksum.
+        // Overwrite them with the received bytes and see if the packet is otherwise identical.
+        request[hdrLen] = buf[hdrLen];          // Type.
+        request[hdrLen + 2] = buf[hdrLen + 2];  // Checksum byte 1.
+        request[hdrLen + 3] = buf[hdrLen + 3];  // Checksum byte 2.
+        for (int i = 0; i < len; i++) {
+            if (buf[i] != request[i]) {
+                Log.i(TAG, "Received non-matching packet when expecting ping response.");
+                return;
+            }
+        }
+
+        // Now swap the addresses again and reflect the packet. This sends a ping reply.
+        swapAddresses(buf, version);
+        writePacket(buf, len);
+    }
+
+    private void writePacket(byte[] buf, int len) {
+        try {
+            Os.write(mFd, buf, 0, len);
+        } catch (ErrnoException|IOException e) {
+            Log.e(TAG, "Error writing packet: " + e.getMessage());
+        }
+    }
+
+    private int readPacket(byte[] buf) {
+        int len;
+        try {
+            len = Os.read(mFd, buf, 0, buf.length);
+        } catch (ErrnoException|IOException e) {
+            Log.e(TAG, "Error reading packet: " + e.getMessage());
+            len = -1;
+        }
+        return len;
+    }
+
+    // Reads one packet from our mFd, and possibly writes the packet back.
+    private void processPacket() {
+        int len = readPacket(mBuf);
+        if (len < 1) {
+            return;
+        }
+
+        int version = mBuf[0] >> 4;
+        int addrPos, protoPos, hdrLen, addrLen;
+        if (version == 4) {
+            hdrLen = IPV4_HEADER_LENGTH;
+            protoPos = IPV4_PROTO_OFFSET;
+            addrPos = IPV4_ADDR_OFFSET;
+            addrLen = IPV4_ADDR_LENGTH;
+        } else if (version == 6) {
+            hdrLen = IPV6_HEADER_LENGTH;
+            protoPos = IPV6_PROTO_OFFSET;
+            addrPos = IPV6_ADDR_OFFSET;
+            addrLen = IPV6_ADDR_LENGTH;
+        } else {
+            return;
+        }
+
+        if (len < hdrLen) {
+            return;
+        }
+
+        byte proto = mBuf[protoPos];
+        switch (proto) {
+            case IPPROTO_ICMP:
+            case IPPROTO_ICMPV6:
+                processIcmpPacket(mBuf, version, len, hdrLen);
+                break;
+            case IPPROTO_TCP:
+                processTcpPacket(mBuf, version, len, hdrLen);
+                break;
+            case IPPROTO_UDP:
+                processUdpPacket(mBuf, version, len, hdrLen);
+                break;
+        }
+    }
+
+    public void run() {
+        Log.i(TAG, "PacketReflector starting fd=" + mFd + " valid=" + mFd.valid());
+        while (!interrupted() && mFd.valid()) {
+            processPacket();
+        }
+        Log.i(TAG, "PacketReflector exiting fd=" + mFd + " valid=" + mFd.valid());
+    }
+}
diff --git a/hostsidetests/net/app/src/com/android/cts/net/hostside/UdpReflector.java b/hostsidetests/net/app/src/com/android/cts/net/hostside/UdpReflector.java
deleted file mode 100644
index a730fed..0000000
--- a/hostsidetests/net/app/src/com/android/cts/net/hostside/UdpReflector.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Copyright (C) 2014 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 com.android.cts.net.hostside;
-
-import android.system.Os;
-import android.system.ErrnoException;
-import android.util.Log;
-
-import java.io.FileDescriptor;
-import java.io.IOException;
-
-public class UdpReflector extends Thread {
-
-    private static int IPV4_HEADER_LENGTH = 20;
-    private static int IPV6_HEADER_LENGTH = 40;
-    private static int UDP_HEADER_LENGTH = 8;
-
-    private static int IPV4_PROTO_OFFSET = 9;
-    private static int IPV6_PROTO_OFFSET = 6;
-    private static int IPPROTO_UDP = 17;
-
-    private static int IPV4_ADDR_OFFSET = 12;
-    private static int IPV6_ADDR_OFFSET = 8;
-    private static int IPV4_ADDR_LENGTH = 4;
-    private static int IPV6_ADDR_LENGTH = 16;
-
-    private static String TAG = "UdpReflector";
-
-    private FileDescriptor mFd;
-    private byte[] mBuf;
-
-    public UdpReflector(FileDescriptor fd, int mtu) {
-        super("UdpReflector");
-        mFd = fd;
-        mBuf = new byte[mtu];
-    }
-
-    private static void swapBytes(byte[] buf, int pos1, int pos2, int len) {
-        for (int i = 0; i < len; i++) {
-            byte b = buf[pos1 + i];
-            buf[pos1 + i] = buf[pos2 + i];
-            buf[pos2 + i] = b;
-        }
-    }
-
-    /** Reads one packet from our mFd, and possibly writes the packet back. */
-    private void processPacket() {
-        int len;
-        try {
-            len = Os.read(mFd, mBuf, 0, mBuf.length);
-        } catch (ErrnoException|IOException e) {
-            Log.e(TAG, "Error reading packet: " + e.getMessage());
-            return;
-        }
-
-        int version = mBuf[0] >> 4;
-        int addressOffset, protoOffset, headerLength, addressLength;
-        if (version == 4) {
-            headerLength = IPV4_HEADER_LENGTH;
-            protoOffset = IPV4_PROTO_OFFSET;
-            addressOffset = IPV4_ADDR_OFFSET;
-            addressLength = IPV4_ADDR_LENGTH;
-        } else if (version == 6) {
-            headerLength = IPV6_HEADER_LENGTH;
-            protoOffset = IPV6_PROTO_OFFSET;
-            addressOffset = IPV6_ADDR_OFFSET;
-            addressLength = IPV6_ADDR_LENGTH;
-        } else {
-            return;
-        }
-
-        if (len < headerLength + UDP_HEADER_LENGTH || mBuf[protoOffset] != IPPROTO_UDP) {
-            return;
-        }
-
-        // Swap src and dst IP addresses.
-        swapBytes(mBuf, addressOffset, addressOffset + addressLength, addressLength);
-
-        // Swap dst and src ports.
-        int portOffset = headerLength;
-        swapBytes(mBuf, portOffset, portOffset + 2, 2);
-
-        // Send the packet back. We don't need to recalculate the checksum because we didn't change
-        // the packet bytes, we only moved them around.
-        try {
-            len = Os.write(mFd, mBuf, 0, len);
-        } catch (ErrnoException|IOException e) {
-            Log.e(TAG, "Error writing packet: " + e.getMessage());
-        }
-    }
-
-    public void run() {
-        Log.i(TAG, "UdpReflector starting fd=" + mFd + " valid=" + mFd.valid());
-        while (!interrupted() && mFd.valid()) {
-            processPacket();
-        }
-        Log.i(TAG, "UdpReflector exiting fd=" + mFd + " valid=" + mFd.valid());
-    }
-}
diff --git a/hostsidetests/net/app/src/com/android/cts/net/hostside/VpnTest.java b/hostsidetests/net/app/src/com/android/cts/net/hostside/VpnTest.java
index cdd370e..8bb2a2c 100644
--- a/hostsidetests/net/app/src/com/android/cts/net/hostside/VpnTest.java
+++ b/hostsidetests/net/app/src/com/android/cts/net/hostside/VpnTest.java
@@ -16,6 +16,8 @@
 
 package com.android.cts.net.hostside;
 
+import static android.system.OsConstants.*;
+
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.net.ConnectivityManager;
@@ -25,34 +27,58 @@
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
 import android.net.VpnService;
-import android.os.ParcelFileDescriptor;
-import android.os.SystemClock;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.UiObject;
 import android.support.test.uiautomator.UiObjectNotFoundException;
 import android.support.test.uiautomator.UiScrollable;
 import android.support.test.uiautomator.UiSelector;
-import android.test.MoreAsserts;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.StructPollfd;
 import android.test.InstrumentationTestCase;
+import android.test.MoreAsserts;
 import android.text.TextUtils;
 import android.util.Log;
 
+import java.io.Closeable;
+import java.io.FileDescriptor;
 import java.io.IOException;
-import java.net.SocketTimeoutException;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.net.DatagramPacket;
 import java.net.DatagramSocket;
-import java.net.InetAddress;
 import java.net.Inet6Address;
-import java.util.concurrent.atomic.AtomicReference;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.Random;
 
 /**
- * Tests for {@link DocumentsProvider} and interaction with platform intents
- * like {@link Intent#ACTION_OPEN_DOCUMENT}.
+ * Tests for the VpnService API.
+ *
+ * These tests establish a VPN via the VpnService API, and have the service reflect the packets back
+ * to the device without causing any network traffic. This allows testing the local VPN data path
+ * without a network connection or a VPN server.
+ *
+ * Note: in Lollipop, VPN functionality relies on kernel support for UID-based routing. If these
+ * tests fail, it may be due to the lack of kernel support. The necessary patches can be
+ * cherry-picked from the Android common kernel trees:
+ *
+ * android-3.10:
+ *   https://android-review.googlesource.com/#/c/99220/
+ *   https://android-review.googlesource.com/#/c/100545/
+ *
+ * android-3.4:
+ *   https://android-review.googlesource.com/#/c/99225/
+ *   https://android-review.googlesource.com/#/c/100557/
+ *
  */
 public class VpnTest extends InstrumentationTestCase {
 
     public static String TAG = "VpnTest";
     public static int TIMEOUT_MS = 3 * 1000;
+    public static int SOCKET_TIMEOUT_MS = 100;
 
     private UiDevice mDevice;
     private MyActivity mActivity;
@@ -201,8 +227,156 @@
         mActivity.startService(intent);
     }
 
-    private static void checkUdpEcho(
-            String to, String expectedFrom, boolean expectReply) throws IOException {
+    private static void closeQuietly(Closeable c) {
+        if (c != null) {
+            try {
+                c.close();
+            } catch (IOException e) {
+            }
+        }
+    }
+
+    private static void checkPing(String to) throws IOException, ErrnoException {
+        InetAddress address = InetAddress.getByName(to);
+        FileDescriptor s;
+        final int LENGTH = 64;
+        byte[] packet = new byte[LENGTH];
+        byte[] header;
+
+        // Construct a ping packet.
+        Random random = new Random();
+        random.nextBytes(packet);
+        if (address instanceof Inet6Address) {
+            s = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6);
+            header = new byte[] { (byte) 0x80, (byte) 0x00, (byte) 0x00, (byte) 0x00 };
+        } else {
+            // Note that this doesn't actually work due to http://b/18558481 .
+            s = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
+            header = new byte[] { (byte) 0x08, (byte) 0x00, (byte) 0x00, (byte) 0x00 };
+        }
+        System.arraycopy(header, 0, packet, 0, header.length);
+
+        // Send the packet.
+        int port = random.nextInt(65534) + 1;
+        Os.connect(s, address, port);
+        Os.write(s, packet, 0, packet.length);
+
+        // Expect a reply.
+        StructPollfd pollfd = new StructPollfd();
+        pollfd.events = (short) POLLIN;  // "error: possible loss of precision"
+        pollfd.fd = s;
+        int ret = Os.poll(new StructPollfd[] { pollfd }, SOCKET_TIMEOUT_MS);
+        assertEquals("Expected reply after sending ping", 1, ret);
+
+        byte[] reply = new byte[LENGTH];
+        int read = Os.read(s, reply, 0, LENGTH);
+        assertEquals(LENGTH, read);
+
+        // Find out what the kernel set the ICMP ID to.
+        InetSocketAddress local = (InetSocketAddress) Os.getsockname(s);
+        port = local.getPort();
+        packet[4] = (byte) ((port >> 8) & 0xff);
+        packet[5] = (byte) (port & 0xff);
+
+        // Check the contents.
+        if (packet[0] == (byte) 0x80) {
+            packet[0] = (byte) 0x81;
+        } else {
+            packet[0] = 0;
+        }
+        // Zero out the checksum in the reply so it matches the uninitialized checksum in packet.
+        reply[2] = reply[3] = 0;
+        MoreAsserts.assertEquals(packet, reply);
+    }
+
+    // Writes data to out and checks that it appears identically on in.
+    private static void writeAndCheckData(
+            OutputStream out, InputStream in, byte[] data) throws IOException {
+        out.write(data, 0, data.length);
+        out.flush();
+
+        byte[] read = new byte[data.length];
+        int bytesRead = 0, totalRead = 0;
+        do {
+            bytesRead = in.read(read, totalRead, read.length - totalRead);
+            totalRead += bytesRead;
+        } while (bytesRead >= 0 && totalRead < data.length);
+        assertEquals(totalRead, data.length);
+        MoreAsserts.assertEquals(data, read);
+    }
+
+    private static void checkTcpReflection(String to, String expectedFrom) throws IOException {
+        // Exercise TCP over the VPN by "connecting to ourselves". We open a server socket and a
+        // client socket, and connect the client socket to a remote host, with the port of the
+        // server socket. The PacketReflector reflects the packets, changing the source addresses
+        // but not the ports, so our client socket is connected to our server socket, though both
+        // sockets think their peers are on the "remote" IP address.
+
+        // Open a listening socket.
+        ServerSocket listen = new ServerSocket(0, 10, InetAddress.getByName("::"));
+
+        // Connect the client socket to it.
+        InetAddress toAddr = InetAddress.getByName(to);
+        Socket client = new Socket();
+        try {
+            client.connect(new InetSocketAddress(toAddr, listen.getLocalPort()), SOCKET_TIMEOUT_MS);
+            if (expectedFrom == null) {
+                closeQuietly(listen);
+                closeQuietly(client);
+                fail("Expected connection to fail, but it succeeded.");
+            }
+        } catch (IOException e) {
+            if (expectedFrom != null) {
+                closeQuietly(listen);
+                fail("Expected connection to succeed, but it failed.");
+            } else {
+                // We expected the connection to fail, and it did, so there's nothing more to test.
+                return;
+            }
+        }
+
+        // The connection succeeded, and we expected it to succeed. Send some data; if things are
+        // working, the data will be sent to the VPN, reflected by the PacketReflector, and arrive
+        // at our server socket. For good measure, send some data in the other direction.
+        Socket server = null;
+        try {
+            // Accept the connection on the server side.
+            listen.setSoTimeout(SOCKET_TIMEOUT_MS);
+            server = listen.accept();
+
+            // Check that the source and peer addresses are as expected.
+            assertEquals(expectedFrom, client.getLocalAddress().getHostAddress());
+            assertEquals(expectedFrom, server.getLocalAddress().getHostAddress());
+            assertEquals(
+                    new InetSocketAddress(toAddr, client.getLocalPort()),
+                    server.getRemoteSocketAddress());
+            assertEquals(
+                    new InetSocketAddress(toAddr, server.getLocalPort()),
+                    client.getRemoteSocketAddress());
+
+            // Now write some data.
+            final int LENGTH = 32768;
+            byte[] data = new byte[LENGTH];
+            new Random().nextBytes(data);
+
+            // Make sure our writes don't block or time out, because we're single-threaded and can't
+            // read and write at the same time.
+            server.setReceiveBufferSize(LENGTH * 2);
+            client.setSendBufferSize(LENGTH * 2);
+            client.setSoTimeout(SOCKET_TIMEOUT_MS);
+            server.setSoTimeout(SOCKET_TIMEOUT_MS);
+
+            // Send some data from client to server, then from server to client.
+            writeAndCheckData(client.getOutputStream(), server.getInputStream(), data);
+            writeAndCheckData(server.getOutputStream(), client.getInputStream(), data);
+        } finally {
+            closeQuietly(listen);
+            closeQuietly(client);
+            closeQuietly(server);
+        }
+    }
+
+    private static void checkUdpEcho(String to, String expectedFrom) throws IOException {
         DatagramSocket s;
         InetAddress address = InetAddress.getByName(to);
         if (address instanceof Inet6Address) {  // http://b/18094870
@@ -210,10 +384,12 @@
         } else {
             s = new DatagramSocket();
         }
-        s.setSoTimeout(100);  // ms.
+        s.setSoTimeout(SOCKET_TIMEOUT_MS);
 
-        String msg = "Hello, world!";
-        DatagramPacket p = new DatagramPacket(msg.getBytes(), msg.length());
+        Random random = new Random();
+        byte[] data = new byte[random.nextInt(1650)];
+        random.nextBytes(data);
+        DatagramPacket p = new DatagramPacket(data, data.length);
         s.connect(address, 7);
 
         if (expectedFrom != null) {
@@ -222,10 +398,10 @@
         }
 
         try {
-            if (expectReply) {
+            if (expectedFrom != null) {
                 s.send(p);
                 s.receive(p);
-                MoreAsserts.assertEquals(msg.getBytes(), p.getData());
+                MoreAsserts.assertEquals(data, p.getData());
             } else {
                 try {
                     s.send(p);
@@ -238,12 +414,19 @@
         }
     }
 
-    private static void expectUdpEcho(String to, String expectedFrom) throws IOException {
-        checkUdpEcho(to, expectedFrom, true);
+    private void checkTrafficOnVpn() throws IOException, ErrnoException {
+        checkUdpEcho("192.0.2.251", "192.0.2.2");
+        checkUdpEcho("2001:db8:dead:beef::f00", "2001:db8:1:2::ffe");
+        checkPing("2001:db8:dead:beef::f00");
+        checkTcpReflection("192.0.2.252", "192.0.2.2");
+        checkTcpReflection("2001:db8:dead:beef::f00", "2001:db8:1:2::ffe");
     }
 
-    private static void expectNoUdpEcho(String to) throws IOException {
-        checkUdpEcho(to, null, false);
+    private void checkNoTrafficOnVpn() throws IOException, ErrnoException {
+        checkUdpEcho("192.0.2.251", null);
+        checkUdpEcho("2001:db8:dead:beef::f00", null);
+        checkTcpReflection("192.0.2.252", null);
+        checkTcpReflection("2001:db8:dead:beef::f00", null);
     }
 
     public void testDefault() throws Exception {
@@ -253,8 +436,7 @@
                  new String[] {"192.0.2.0/24", "2001:db8::/32"},
                  "", "");
 
-        expectUdpEcho("192.0.2.251", "192.0.2.2");
-        expectUdpEcho("2001:db8:dead:beef::f00", "2001:db8:1:2::ffe");
+        checkTrafficOnVpn();
     }
 
     public void testAppAllowed() throws Exception {
@@ -264,8 +446,7 @@
                  new String[] {"0.0.0.0/0", "::/0"},
                  mPackageName, "");
 
-        expectUdpEcho("192.0.2.251", "192.0.2.2");
-        expectUdpEcho("2001:db8:dead:beef::f00", "2001:db8:1:2::ffe");
+        checkTrafficOnVpn();
     }
 
     public void testAppDisallowed() throws Exception {
@@ -275,7 +456,6 @@
                  new String[] {"192.0.2.0/24", "2001:db8::/32"},
                  "", mPackageName);
 
-        expectNoUdpEcho("192.0.2.251");
-        expectNoUdpEcho("2001:db8:dead:beef::f00");
+        checkNoTrafficOnVpn();
     }
 }