Add CTS tests for exclude VPN routes APIs
Add HostsideVpnTests test cases for different combinations of
included/excluded routes in VPN configuration.
Bug: 186082280
Test: atest HostsideVpnTests
Change-Id: I993a32d4066a851eb0c455aff2f599569e958ba7
diff --git a/tests/cts/hostside/Android.bp b/tests/cts/hostside/Android.bp
index f72a458..b684068 100644
--- a/tests/cts/hostside/Android.bp
+++ b/tests/cts/hostside/Android.bp
@@ -25,6 +25,9 @@
"cts-tradefed",
"tradefed",
],
+ static_libs: [
+ "modules-utils-build-testing",
+ ],
// Tag this module as a cts test artifact
test_suites: [
"cts",
diff --git a/tests/cts/hostside/app/Android.bp b/tests/cts/hostside/app/Android.bp
index 674af14..8597bff 100644
--- a/tests/cts/hostside/app/Android.bp
+++ b/tests/cts/hostside/app/Android.bp
@@ -18,12 +18,8 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
-android_test_helper_app {
- name: "CtsHostsideNetworkTestsApp",
- defaults: [
- "cts_support_defaults",
- "framework-connectivity-test-defaults",
- ],
+java_defaults {
+ name: "CtsHostsideNetworkTestsAppDefaults",
platform_apis: true,
static_libs: [
"CtsHostsideNetworkTestsAidl",
@@ -47,3 +43,28 @@
"general-tests",
],
}
+
+android_test_helper_app {
+ name: "CtsHostsideNetworkTestsApp",
+ defaults: [
+ "cts_support_defaults",
+ "framework-connectivity-test-defaults",
+ "CtsHostsideNetworkTestsAppDefaults",
+ ],
+ static_libs: [
+ "NetworkStackApiStableShims",
+ ],
+}
+
+android_test_helper_app {
+ name: "CtsHostsideNetworkTestsAppNext",
+ defaults: [
+ "cts_support_defaults",
+ "framework-connectivity-test-defaults",
+ "CtsHostsideNetworkTestsAppDefaults",
+ "ConnectivityNextEnableDefaults",
+ ],
+ static_libs: [
+ "NetworkStackApiCurrentShims",
+ ],
+}
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
index 7d3d4fc..7adc71a 100644
--- 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
@@ -17,18 +17,27 @@
package com.android.cts.net.hostside;
import android.content.Intent;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.IpPrefix;
import android.net.Network;
+import android.net.NetworkUtils;
import android.net.ProxyInfo;
import android.net.VpnService;
import android.os.ParcelFileDescriptor;
-import android.content.pm.PackageManager.NameNotFoundException;
import android.text.TextUtils;
import android.util.Log;
+import android.util.Pair;
+
+import com.android.modules.utils.build.SdkLevel;
+import com.android.networkstack.apishim.VpnServiceBuilderShimImpl;
+import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
+import com.android.networkstack.apishim.common.VpnServiceBuilderShim;
import java.io.IOException;
import java.net.InetAddress;
-import java.net.UnknownHostException;
import java.util.ArrayList;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
public class MyVpnService extends VpnService {
@@ -55,39 +64,62 @@
return START_NOT_STICKY;
}
- private void start(String packageName, Intent intent) {
- Builder builder = new Builder();
+ private String parseIpAndMaskListArgument(String packageName, Intent intent, String argName,
+ BiConsumer<InetAddress, Integer> consumer) {
+ final String addresses = intent.getStringExtra(packageName + "." + argName);
- 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;
- }
- }
+ if (TextUtils.isEmpty(addresses)) {
+ return null;
}
- 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("/");
+ final String[] addressesArray = addresses.split(",");
+ for (String address : addressesArray) {
+ final Pair<InetAddress, Integer> ipAndMask = NetworkUtils.parseIpAndMask(address);
+ consumer.accept(ipAndMask.first, ipAndMask.second);
+ }
+
+ return addresses;
+ }
+
+ private String parseIpPrefixListArgument(String packageName, Intent intent, String argName,
+ Consumer<IpPrefix> consumer) {
+ return parseIpAndMaskListArgument(packageName, intent, argName,
+ (inetAddress, prefixLength) -> consumer.accept(
+ new IpPrefix(inetAddress, prefixLength)));
+ }
+
+ private void start(String packageName, Intent intent) {
+ Builder builder = new Builder();
+ VpnServiceBuilderShim vpnServiceBuilderShim = VpnServiceBuilderShimImpl.newInstance();
+
+ final String addresses = parseIpAndMaskListArgument(packageName, intent, "addresses",
+ builder::addAddress);
+
+ String addedRoutes;
+ if (SdkLevel.isAtLeastT() && intent.getBooleanExtra(packageName + ".addRoutesByIpPrefix",
+ false)) {
+ addedRoutes = parseIpPrefixListArgument(packageName, intent, "routes", (prefix) -> {
try {
- InetAddress address = InetAddress.getByName(prefixAndMask[0]);
- int prefixLength = Integer.parseInt(prefixAndMask[1]);
- builder.addRoute(address, prefixLength);
- } catch (UnknownHostException|NumberFormatException|
- ArrayIndexOutOfBoundsException e) {
- continue;
+ vpnServiceBuilderShim.addRoute(builder, prefix);
+ } catch (UnsupportedApiLevelException e) {
+ throw new RuntimeException(e);
}
- }
+ });
+ } else {
+ addedRoutes = parseIpAndMaskListArgument(packageName, intent, "routes",
+ builder::addRoute);
+ }
+
+ String excludedRoutes = null;
+ if (SdkLevel.isAtLeastT()) {
+ excludedRoutes = parseIpPrefixListArgument(packageName, intent, "excludedRoutes",
+ (prefix) -> {
+ try {
+ vpnServiceBuilderShim.excludeRoute(builder, prefix);
+ } catch (UnsupportedApiLevelException e) {
+ throw new RuntimeException(e);
+ }
+ });
}
String allowed = intent.getStringExtra(packageName + ".allowedapplications");
@@ -140,7 +172,8 @@
Log.i(TAG, "Establishing VPN,"
+ " addresses=" + addresses
- + " routes=" + routes
+ + " addedRoutes=" + addedRoutes
+ + " excludedRoutes=" + excludedRoutes
+ " allowedApplications=" + allowed
+ " disallowedApplications=" + disallowed);
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 3abc4fb..1841200 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
@@ -268,9 +268,32 @@
// TODO: Consider replacing arguments with a Builder.
private void startVpn(
- String[] addresses, String[] routes, String allowedApplications,
- String disallowedApplications, @Nullable ProxyInfo proxyInfo,
- @Nullable ArrayList<Network> underlyingNetworks, boolean isAlwaysMetered) throws Exception {
+ String[] addresses, String[] routes, String allowedApplications,
+ String disallowedApplications, @Nullable ProxyInfo proxyInfo,
+ @Nullable ArrayList<Network> underlyingNetworks, boolean isAlwaysMetered)
+ throws Exception {
+ startVpn(addresses, routes, new String[0] /* excludedRoutes */, allowedApplications,
+ disallowedApplications, proxyInfo, underlyingNetworks, isAlwaysMetered);
+ }
+
+ private void startVpn(
+ String[] addresses, String[] routes, String[] excludedRoutes,
+ String allowedApplications, String disallowedApplications,
+ @Nullable ProxyInfo proxyInfo,
+ @Nullable ArrayList<Network> underlyingNetworks, boolean isAlwaysMetered)
+ throws Exception {
+ startVpn(addresses, routes, new String[0] /* excludedRoutes */, allowedApplications,
+ disallowedApplications, proxyInfo, underlyingNetworks, isAlwaysMetered,
+ false /* addRoutesByIpPrefix */);
+ }
+
+ private void startVpn(
+ String[] addresses, String[] routes, String[] excludedRoutes,
+ String allowedApplications, String disallowedApplications,
+ @Nullable ProxyInfo proxyInfo,
+ @Nullable ArrayList<Network> underlyingNetworks, boolean isAlwaysMetered,
+ boolean addRoutesByIpPrefix)
+ throws Exception {
prepareVpn();
// Register a callback so we will be notified when our VPN comes up.
@@ -295,12 +318,14 @@
.putExtra(mPackageName + ".cmd", "connect")
.putExtra(mPackageName + ".addresses", TextUtils.join(",", addresses))
.putExtra(mPackageName + ".routes", TextUtils.join(",", routes))
+ .putExtra(mPackageName + ".excludedRoutes", TextUtils.join(",", excludedRoutes))
.putExtra(mPackageName + ".allowedapplications", allowedApplications)
.putExtra(mPackageName + ".disallowedapplications", disallowedApplications)
.putExtra(mPackageName + ".httpProxy", proxyInfo)
.putParcelableArrayListExtra(
mPackageName + ".underlyingNetworks", underlyingNetworks)
- .putExtra(mPackageName + ".isAlwaysMetered", isAlwaysMetered);
+ .putExtra(mPackageName + ".isAlwaysMetered", isAlwaysMetered)
+ .putExtra(mPackageName + ".addRoutesByIpPrefix", addRoutesByIpPrefix);
mActivity.startService(intent);
synchronized (mLock) {
@@ -522,6 +547,12 @@
}
private void checkUdpEcho(String to, String expectedFrom) throws IOException {
+ checkUdpEcho(to, expectedFrom, expectedFrom != null);
+ }
+
+ private void checkUdpEcho(String to, String expectedFrom,
+ boolean expectConnectionOwnerIsVisible)
+ throws IOException {
DatagramSocket s;
InetAddress address = InetAddress.getByName(to);
if (address instanceof Inet6Address) { // http://b/18094870
@@ -545,7 +576,7 @@
try {
if (expectedFrom != null) {
s.send(p);
- checkConnectionOwnerUidUdp(s, true);
+ checkConnectionOwnerUidUdp(s, expectConnectionOwnerIsVisible);
s.receive(p);
MoreAsserts.assertEquals(data, p.getData());
} else {
@@ -554,7 +585,7 @@
s.receive(p);
fail("Received unexpected reply");
} catch (IOException expected) {
- checkConnectionOwnerUidUdp(s, false);
+ checkConnectionOwnerUidUdp(s, expectConnectionOwnerIsVisible);
}
}
} finally {
@@ -562,19 +593,38 @@
}
}
+ private void checkTrafficOnVpn(String destination) throws Exception {
+ final InetAddress address = InetAddress.getByName(destination);
+
+ if (address instanceof Inet6Address) {
+ checkUdpEcho(destination, "2001:db8:1:2::ffe");
+ checkTcpReflection(destination, "2001:db8:1:2::ffe");
+ checkPing(destination);
+ } else {
+ checkUdpEcho(destination, "192.0.2.2");
+ checkTcpReflection(destination, "192.0.2.2");
+ }
+
+ }
+
+ private void checkNoTrafficOnVpn(String destination) throws IOException {
+ checkUdpEcho(destination, null);
+ checkTcpReflection(destination, null);
+ }
+
private void checkTrafficOnVpn() throws Exception {
- 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");
+ checkTrafficOnVpn("192.0.2.251");
+ checkTrafficOnVpn("2001:db8:dead:beef::f00");
}
private void checkNoTrafficOnVpn() throws Exception {
- 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);
+ checkNoTrafficOnVpn("192.0.2.251");
+ checkNoTrafficOnVpn("2001:db8:dead:beef::f00");
+ }
+
+ private void checkTrafficBypassesVpn(String destination) throws Exception {
+ checkUdpEcho(destination, null, true /* expectVpnOwnedConnection */);
+ checkTcpReflection(destination, null);
}
private FileDescriptor openSocketFd(String host, int port, int timeoutMs) throws Exception {
@@ -858,9 +908,9 @@
}
Log.i(TAG, "Append shell app to disallowedApps: " + disallowedApps);
startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
- new String[] {"192.0.2.0/24", "2001:db8::/32"},
- "", disallowedApps, null, null /* underlyingNetworks */,
- false /* isAlwaysMetered */);
+ new String[] {"192.0.2.0/24", "2001:db8::/32"},
+ "", disallowedApps, null, null /* underlyingNetworks */,
+ false /* isAlwaysMetered */);
assertSocketStillOpen(localFd, TEST_HOST);
assertSocketStillOpen(remoteFd, TEST_HOST);
@@ -873,6 +923,74 @@
}
@Test
+ public void testExcludedRoutes() throws Exception {
+ if (!supportedHardware()) return;
+ if (!SdkLevel.isAtLeastT()) return;
+
+ // Shell app must not be put in here or it would kill the ADB-over-network use case
+ String allowedApps = mRemoteSocketFactoryClient.getPackageName() + "," + mPackageName;
+ startVpn(new String[]{"192.0.2.2/32", "2001:db8:1:2::ffe/128"} /* addresses */,
+ new String[]{"0.0.0.0/0", "::/0"} /* routes */,
+ new String[]{"192.0.2.0/24", "2001:db8::/32"} /* excludedRoutes */,
+ allowedApps, "" /* disallowedApplications */, null /* proxyInfo */,
+ null /* underlyingNetworks */, false /* isAlwaysMetered */);
+
+ // Excluded routes should bypass VPN.
+ checkTrafficBypassesVpn("192.0.2.1");
+ checkTrafficBypassesVpn("2001:db8:dead:beef::f00");
+ // Other routes should go through VPN, since default routes are included.
+ checkTrafficOnVpn("198.51.100.1");
+ checkTrafficOnVpn("2002:db8::1");
+ }
+
+ @Test
+ public void testIncludedRoutes() throws Exception {
+ if (!supportedHardware()) return;
+
+ // Shell app must not be put in here or it would kill the ADB-over-network use case
+ String allowedApps = mRemoteSocketFactoryClient.getPackageName() + "," + mPackageName;
+ startVpn(new String[]{"192.0.2.2/32", "2001:db8:1:2::ffe/128"} /* addresses */,
+ new String[]{"192.0.2.0/24", "2001:db8::/32"} /* routes */,
+ allowedApps, "" /* disallowedApplications */, null /* proxyInfo */,
+ null /* underlyingNetworks */, false /* isAlwaysMetered */);
+
+ // Included routes should go through VPN.
+ checkTrafficOnVpn("192.0.2.1");
+ checkTrafficOnVpn("2001:db8:dead:beef::f00");
+ // Other routes should bypass VPN, since default routes are not included.
+ checkTrafficBypassesVpn("198.51.100.1");
+ checkTrafficBypassesVpn("2002:db8::1");
+ }
+
+ @Test
+ public void testInterleavedRoutes() throws Exception {
+ if (!supportedHardware()) return;
+ if (!SdkLevel.isAtLeastT()) return;
+
+ // Shell app must not be put in here or it would kill the ADB-over-network use case
+ String allowedApps = mRemoteSocketFactoryClient.getPackageName() + "," + mPackageName;
+ startVpn(new String[]{"192.0.2.2/32", "2001:db8:1:2::ffe/128"} /* addresses */,
+ new String[]{"0.0.0.0/0", "192.0.2.0/32", "::/0", "2001:db8::/128"} /* routes */,
+ new String[]{"192.0.2.0/24", "2001:db8::/32"} /* excludedRoutes */,
+ allowedApps, "" /* disallowedApplications */, null /* proxyInfo */,
+ null /* underlyingNetworks */, false /* isAlwaysMetered */,
+ true /* addRoutesByIpPrefix */);
+
+ // Excluded routes should bypass VPN.
+ checkTrafficBypassesVpn("192.0.2.1");
+ checkTrafficBypassesVpn("2001:db8:dead:beef::f00");
+
+ // Included routes inside excluded routes should go through VPN, since the longest common
+ // prefix precedes.
+ checkTrafficOnVpn("192.0.2.0");
+ checkTrafficOnVpn("2001:db8::");
+
+ // Other routes should go through VPN, since default routes are included.
+ checkTrafficOnVpn("198.51.100.1");
+ checkTrafficOnVpn("2002:db8::1");
+ }
+
+ @Test
public void testGetConnectionOwnerUidSecurity() throws Exception {
if (!supportedHardware()) return;
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
index 89c79d3..cc07fd1 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
@@ -20,6 +20,7 @@
import com.android.ddmlib.Log;
import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.modules.utils.build.testing.DeviceSdkLevel;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.result.CollectingTestListener;
@@ -42,6 +43,7 @@
protected static final String TAG = "HostsideNetworkTests";
protected static final String TEST_PKG = "com.android.cts.net.hostside";
protected static final String TEST_APK = "CtsHostsideNetworkTestsApp.apk";
+ protected static final String TEST_APK_NEXT = "CtsHostsideNetworkTestsAppNext.apk";
protected static final String TEST_APP2_PKG = "com.android.cts.net.hostside.app2";
protected static final String TEST_APP2_APK = "CtsHostsideNetworkTestsApp2.apk";
@@ -65,8 +67,12 @@
assertNotNull(mAbi);
assertNotNull(mCtsBuild);
+ DeviceSdkLevel deviceSdkLevel = new DeviceSdkLevel(getDevice());
+ String testApk = deviceSdkLevel.isDeviceAtLeastT() ? TEST_APK_NEXT
+ : TEST_APK;
+
uninstallPackage(TEST_PKG, false);
- installPackage(TEST_APK);
+ installPackage(testApk);
}
@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 49b5f9d..24840d2 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
@@ -100,4 +100,16 @@
runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest",
"testDownloadWithDownloadManagerDisallowed");
}
+
+ public void testExcludedRoutes() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testExcludedRoutes");
+ }
+
+ public void testIncludedRoutes() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testIncludedRoutes");
+ }
+
+ public void testInterleavedRoutes() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testInterleavedRoutes");
+ }
}