Add a CTS test for the VPN API.
Bug: 15605143
Change-Id: I8e5f8b281b6ee16acf8daf1b4a1113847e1ccabd
diff --git a/tests/cts/hostside/Android.mk b/tests/cts/hostside/Android.mk
new file mode 100644
index 0000000..6637d61
--- /dev/null
+++ b/tests/cts/hostside/Android.mk
@@ -0,0 +1,31 @@
+# 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.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+# Only compile source java files in this apk.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_MODULE := CtsHostsideNetworkTests
+
+LOCAL_JAVA_LIBRARIES := cts-tradefed tradefed-prebuilt
+
+LOCAL_CTS_TEST_PACKAGE := android.net.hostsidenetwork
+
+include $(BUILD_CTS_HOST_JAVA_LIBRARY)
+
+# Build the test APKs using their own makefiles
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/tests/cts/hostside/app/Android.mk b/tests/cts/hostside/app/Android.mk
new file mode 100644
index 0000000..29b620d
--- /dev/null
+++ b/tests/cts/hostside/app/Android.mk
@@ -0,0 +1,32 @@
+#
+# 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.
+#
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+LOCAL_SDK_VERSION := current
+LOCAL_STATIC_JAVA_LIBRARIES := ctsdeviceutil ctstestrunner ub-uiautomator
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := CtsHostsideNetworkTestsApp
+
+LOCAL_PROGUARD_ENABLED := disabled
+LOCAL_DEX_PREOPT := false
+
+include $(BUILD_PACKAGE)
diff --git a/tests/cts/hostside/app/AndroidManifest.xml b/tests/cts/hostside/app/AndroidManifest.xml
new file mode 100644
index 0000000..cdde7dc
--- /dev/null
+++ b/tests/cts/hostside/app/AndroidManifest.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.cts.net.hostside">
+
+ <uses-permission android:name="android.permission.INTERNET"/>
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ <activity android:name=".MyActivity" />
+ <service android:name=".MyVpnService"
+ android:permission="android.permission.BIND_VPN_SERVICE">
+ <intent-filter>
+ <action android:name="android.net.VpnService"/>
+ </intent-filter>
+ </service>
+ </application>
+
+ <instrumentation
+ android:name="android.support.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.cts.net.hostside" />
+
+</manifest>
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyActivity.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyActivity.java
new file mode 100644
index 0000000..375c852
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyActivity.java
@@ -0,0 +1,53 @@
+/*
+ * 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.app.Activity;
+import android.content.Intent;
+import android.net.VpnService;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+import android.view.WindowManager;
+
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+public class MyActivity extends Activity {
+ private final LinkedBlockingQueue<Integer> mResult = new LinkedBlockingQueue<>(1);
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+ | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+ | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (mResult.offer(resultCode) == false) {
+ throw new RuntimeException("Queue is full! This should never happen");
+ }
+ }
+
+ public Integer getResult(int timeoutMs) throws InterruptedException {
+ return mResult.poll(timeoutMs, TimeUnit.MILLISECONDS);
+ }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java
new file mode 100644
index 0000000..1a12aaa
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java
@@ -0,0 +1,155 @@
+/*
+ * 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.content.Intent;
+import android.net.VpnService;
+import android.os.ParcelFileDescriptor;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+
+public class MyVpnService extends VpnService {
+
+ private static String TAG = "MyVpnService";
+ private static int MTU = 1799;
+
+ private ParcelFileDescriptor mFd = null;
+ private UdpReflector mUdpReflector = null;
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ String packageName = getPackageName();
+ String cmd = intent.getStringExtra(packageName + ".cmd");
+ if ("disconnect".equals(cmd)) {
+ stop();
+ } else if ("connect".equals(cmd)) {
+ start(packageName, intent);
+ }
+
+ return START_NOT_STICKY;
+ }
+
+ private void start(String packageName, Intent intent) {
+ Builder builder = new Builder();
+
+ String addresses = intent.getStringExtra(packageName + ".addresses");
+ if (addresses != null) {
+ String[] addressArray = addresses.split(",");
+ for (int i = 0; i < addressArray.length; i++) {
+ String[] prefixAndMask = addressArray[i].split("/");
+ try {
+ InetAddress address = InetAddress.getByName(prefixAndMask[0]);
+ int prefixLength = Integer.parseInt(prefixAndMask[1]);
+ builder.addAddress(address, prefixLength);
+ } catch (UnknownHostException|NumberFormatException|
+ ArrayIndexOutOfBoundsException e) {
+ continue;
+ }
+ }
+ }
+
+ String routes = intent.getStringExtra(packageName + ".routes");
+ if (routes != null) {
+ String[] routeArray = routes.split(",");
+ for (int i = 0; i < routeArray.length; i++) {
+ String[] prefixAndMask = routeArray[i].split("/");
+ try {
+ InetAddress address = InetAddress.getByName(prefixAndMask[0]);
+ int prefixLength = Integer.parseInt(prefixAndMask[1]);
+ builder.addRoute(address, prefixLength);
+ } catch (UnknownHostException|NumberFormatException|
+ ArrayIndexOutOfBoundsException e) {
+ continue;
+ }
+ }
+ }
+
+ String allowed = intent.getStringExtra(packageName + ".allowedapplications");
+ if (allowed != null) {
+ String[] packageArray = allowed.split(",");
+ for (int i = 0; i < packageArray.length; i++) {
+ String allowedPackage = packageArray[i];
+ if (!TextUtils.isEmpty(allowedPackage)) {
+ try {
+ builder.addAllowedApplication(allowedPackage);
+ } catch(NameNotFoundException e) {
+ continue;
+ }
+ }
+ }
+ }
+
+ String disallowed = intent.getStringExtra(packageName + ".disallowedapplications");
+ if (disallowed != null) {
+ String[] packageArray = disallowed.split(",");
+ for (int i = 0; i < packageArray.length; i++) {
+ String disallowedPackage = packageArray[i];
+ if (!TextUtils.isEmpty(disallowedPackage)) {
+ try {
+ builder.addDisallowedApplication(disallowedPackage);
+ } catch(NameNotFoundException e) {
+ continue;
+ }
+ }
+ }
+ }
+
+ builder.setMtu(MTU);
+ builder.setBlocking(true);
+ builder.setSession("MyVpnService");
+
+ Log.i(TAG, "Establishing VPN,"
+ + " addresses=" + addresses
+ + " routes=" + routes
+ + " allowedApplications=" + allowed
+ + " disallowedApplications=" + disallowed);
+
+ mFd = builder.establish();
+ Log.i(TAG, "Established, fd=" + (mFd == null ? "null" : mFd.getFd()));
+
+ mUdpReflector = new UdpReflector(mFd.getFileDescriptor(), MTU);
+ mUdpReflector.start();
+ }
+
+ private void stop() {
+ if (mUdpReflector != null) {
+ mUdpReflector.interrupt();
+ mUdpReflector = null;
+ }
+ try {
+ if (mFd != null) {
+ Log.i(TAG, "Closing filedescriptor");
+ mFd.close();
+ }
+ } catch(IOException e) {
+ } finally {
+ mFd = null;
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ stop();
+ super.onDestroy();
+ }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/UdpReflector.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/UdpReflector.java
new file mode 100644
index 0000000..a730fed
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/UdpReflector.java
@@ -0,0 +1,113 @@
+/*
+ * 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/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
new file mode 100644
index 0000000..cdd370e
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
@@ -0,0 +1,281 @@
+/*
+ * 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.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.LinkProperties;
+import android.net.Network;
+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.test.InstrumentationTestCase;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.SocketTimeoutException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.Inet6Address;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Tests for {@link DocumentsProvider} and interaction with platform intents
+ * like {@link Intent#ACTION_OPEN_DOCUMENT}.
+ */
+public class VpnTest extends InstrumentationTestCase {
+
+ public static String TAG = "VpnTest";
+ public static int TIMEOUT_MS = 3 * 1000;
+
+ private UiDevice mDevice;
+ private MyActivity mActivity;
+ private String mPackageName;
+ private ConnectivityManager mCM;
+ Network mNetwork;
+ NetworkCallback mCallback;
+ final Object mLock = new Object();
+
+ private boolean supportedHardware() {
+ final PackageManager pm = getInstrumentation().getContext().getPackageManager();
+ return !pm.hasSystemFeature("android.hardware.type.television") &&
+ !pm.hasSystemFeature("android.hardware.type.watch");
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mNetwork = null;
+ mCallback = null;
+
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mActivity = launchActivity(getInstrumentation().getTargetContext().getPackageName(),
+ MyActivity.class, null);
+ mPackageName = mActivity.getPackageName();
+ mCM = (ConnectivityManager) mActivity.getSystemService(mActivity.CONNECTIVITY_SERVICE);
+ mDevice.waitForIdle();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ if (mCallback != null) {
+ mCM.unregisterNetworkCallback(mCallback);
+ }
+ Log.i(TAG, "Stopping VPN");
+ stopVpn();
+ mActivity.finish();
+ super.tearDown();
+ }
+
+ private void prepareVpn() throws Exception {
+ final int REQUEST_ID = 42;
+
+ // Attempt to prepare.
+ Log.i(TAG, "Preparing VPN");
+ Intent intent = VpnService.prepare(mActivity);
+
+ if (intent != null) {
+ // Start the confirmation dialog and click OK.
+ mActivity.startActivityForResult(intent, REQUEST_ID);
+ mDevice.waitForIdle();
+
+ String packageName = intent.getComponent().getPackageName();
+ String resourceIdRegex = "android:id/button1$|button_start_vpn";
+ final UiObject okButton = new UiObject(new UiSelector()
+ .className("android.widget.Button")
+ .packageName(packageName)
+ .resourceIdMatches(resourceIdRegex));
+ if (okButton.waitForExists(TIMEOUT_MS) == false) {
+ mActivity.finishActivity(REQUEST_ID);
+ fail("VpnService.prepare returned an Intent for '" + intent.getComponent() + "' " +
+ "to display the VPN confirmation dialog, but this test could not find the " +
+ "button to allow the VPN application to connect. Please ensure that the " +
+ "component displays a button with a resource ID matching the regexp: '" +
+ resourceIdRegex + "'.");
+ }
+
+ // Click the button and wait for RESULT_OK.
+ okButton.click();
+ try {
+ int result = mActivity.getResult(TIMEOUT_MS);
+ if (result != MyActivity.RESULT_OK) {
+ fail("The VPN confirmation dialog did not return RESULT_OK when clicking on " +
+ "the button matching the regular expression '" + resourceIdRegex +
+ "' of " + intent.getComponent() + "'. Please ensure that clicking on " +
+ "that button allows the VPN application to connect. " +
+ "Return value: " + result);
+ }
+ } catch (InterruptedException e) {
+ fail("VPN confirmation dialog did not return after " + TIMEOUT_MS + "ms");
+ }
+
+ // Now we should be prepared.
+ intent = VpnService.prepare(mActivity);
+ if (intent != null) {
+ fail("VpnService.prepare returned non-null even after the VPN dialog " +
+ intent.getComponent() + "returned RESULT_OK.");
+ }
+ }
+ }
+
+ private void startVpn(
+ String[] addresses, String[] routes,
+ String allowedApplications, String disallowedApplications) throws Exception {
+
+ prepareVpn();
+
+ // Register a callback so we will be notified when our VPN comes up.
+ final NetworkRequest request = new NetworkRequest.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_VPN)
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .build();
+ mCallback = new NetworkCallback() {
+ public void onAvailable(Network network) {
+ synchronized (mLock) {
+ Log.i(TAG, "Got available callback for network=" + network);
+ mNetwork = network;
+ mLock.notify();
+ }
+ }
+ };
+ mCM.registerNetworkCallback(request, mCallback); // Unregistered in tearDown.
+
+ // Start the service and wait up for TIMEOUT_MS ms for the VPN to come up.
+ Intent intent = new Intent(mActivity, MyVpnService.class)
+ .putExtra(mPackageName + ".cmd", "connect")
+ .putExtra(mPackageName + ".addresses", TextUtils.join(",", addresses))
+ .putExtra(mPackageName + ".routes", TextUtils.join(",", routes))
+ .putExtra(mPackageName + ".allowedapplications", allowedApplications)
+ .putExtra(mPackageName + ".disallowedapplications", disallowedApplications);
+ mActivity.startService(intent);
+ synchronized (mLock) {
+ if (mNetwork == null) {
+ mLock.wait(TIMEOUT_MS);
+ }
+ }
+
+ if (mNetwork == null) {
+ fail("VPN did not become available after " + TIMEOUT_MS + "ms");
+ }
+
+ // Unfortunately, when the available callback fires, the VPN UID ranges are not yet
+ // configured. Give the system some time to do so. http://b/18436087 .
+ try { Thread.sleep(300); } catch(InterruptedException e) {}
+ }
+
+ private void stopVpn() {
+ // Simply calling mActivity.stopService() won't stop the service, because the system binds
+ // to the service for the purpose of sending it a revoke command if another VPN comes up,
+ // and stopping a bound service has no effect. Instead, "start" the service again with an
+ // Intent that tells it to disconnect.
+ Intent intent = new Intent(mActivity, MyVpnService.class)
+ .putExtra(mPackageName + ".cmd", "disconnect");
+ mActivity.startService(intent);
+ }
+
+ private static void checkUdpEcho(
+ String to, String expectedFrom, boolean expectReply) throws IOException {
+ DatagramSocket s;
+ InetAddress address = InetAddress.getByName(to);
+ if (address instanceof Inet6Address) { // http://b/18094870
+ s = new DatagramSocket(0, InetAddress.getByName("::"));
+ } else {
+ s = new DatagramSocket();
+ }
+ s.setSoTimeout(100); // ms.
+
+ String msg = "Hello, world!";
+ DatagramPacket p = new DatagramPacket(msg.getBytes(), msg.length());
+ s.connect(address, 7);
+
+ if (expectedFrom != null) {
+ assertEquals("Unexpected source address: ",
+ expectedFrom, s.getLocalAddress().getHostAddress());
+ }
+
+ try {
+ if (expectReply) {
+ s.send(p);
+ s.receive(p);
+ MoreAsserts.assertEquals(msg.getBytes(), p.getData());
+ } else {
+ try {
+ s.send(p);
+ s.receive(p);
+ fail("Received unexpected reply");
+ } catch(IOException expected) {}
+ }
+ } finally {
+ s.close();
+ }
+ }
+
+ private static void expectUdpEcho(String to, String expectedFrom) throws IOException {
+ checkUdpEcho(to, expectedFrom, true);
+ }
+
+ private static void expectNoUdpEcho(String to) throws IOException {
+ checkUdpEcho(to, null, false);
+ }
+
+ public void testDefault() throws Exception {
+ if (!supportedHardware()) return;
+
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
+ 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");
+ }
+
+ public void testAppAllowed() throws Exception {
+ if (!supportedHardware()) return;
+
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
+ 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");
+ }
+
+ public void testAppDisallowed() throws Exception {
+ if (!supportedHardware()) return;
+
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
+ new String[] {"192.0.2.0/24", "2001:db8::/32"},
+ "", mPackageName);
+
+ expectNoUdpEcho("192.0.2.251");
+ expectNoUdpEcho("2001:db8:dead:beef::f00");
+ }
+}
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTests.java
new file mode 100644
index 0000000..a7698f3
--- /dev/null
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTests.java
@@ -0,0 +1,105 @@
+/*
+ * 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;
+
+import com.android.cts.tradefed.build.CtsBuildHelper;
+import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
+import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.ddmlib.testrunner.TestResult;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.ddmlib.testrunner.TestRunResult;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IAbi;
+import com.android.tradefed.testtype.IAbiReceiver;
+import com.android.tradefed.testtype.IBuildReceiver;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.result.CollectingTestListener;
+
+import java.util.Map;
+
+public class HostsideNetworkTests extends DeviceTestCase implements IAbiReceiver, IBuildReceiver {
+ private static final String TEST_PKG = "com.android.cts.net.hostside";
+ private static final String TEST_APK = "CtsHostsideNetworkTestsApp.apk";
+
+ private IAbi mAbi;
+ private CtsBuildHelper mCtsBuild;
+
+ @Override
+ public void setAbi(IAbi abi) {
+ mAbi = abi;
+ }
+
+ @Override
+ public void setBuild(IBuildInfo buildInfo) {
+ mCtsBuild = CtsBuildHelper.createBuildHelper(buildInfo);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ assertNotNull(mAbi);
+ assertNotNull(mCtsBuild);
+
+ getDevice().uninstallPackage(TEST_PKG);
+
+ assertNull(getDevice().installPackage(mCtsBuild.getTestApp(TEST_APK), false));
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+
+ getDevice().uninstallPackage(TEST_PKG);
+ }
+
+ public void testVpn() throws Exception {
+ runDeviceTests(TEST_PKG, ".VpnTest");
+ }
+
+ public void runDeviceTests(String packageName, String testClassName)
+ throws DeviceNotAvailableException {
+ RemoteAndroidTestRunner testRunner = new RemoteAndroidTestRunner(packageName,
+ "android.support.test.runner.AndroidJUnitRunner", getDevice().getIDevice());
+
+ final CollectingTestListener listener = new CollectingTestListener();
+ getDevice().runInstrumentationTests(testRunner, listener);
+
+ final TestRunResult result = listener.getCurrentRunResults();
+ if (result.isRunFailure()) {
+ throw new AssertionError("Failed to successfully run device tests for "
+ + result.getName() + ": " + result.getRunFailureMessage());
+ }
+
+ if (result.hasFailedTests()) {
+ // build a meaningful error message
+ StringBuilder errorBuilder = new StringBuilder("on-device tests failed:\n");
+ for (Map.Entry<TestIdentifier, TestResult> resultEntry :
+ result.getTestResults().entrySet()) {
+ if (!resultEntry.getValue().getStatus().equals(TestStatus.PASSED)) {
+ errorBuilder.append(resultEntry.getKey().toString());
+ errorBuilder.append(":\n");
+ errorBuilder.append(resultEntry.getValue().getStackTrace());
+ }
+ }
+ throw new AssertionError(errorBuilder.toString());
+ }
+ }
+}