diff --git a/tests/cts/hostside/app/AndroidManifest.xml b/tests/cts/hostside/app/AndroidManifest.xml index e5bae5fdc0..d56e5d4651 100644 --- a/tests/cts/hostside/app/AndroidManifest.xml +++ b/tests/cts/hostside/app/AndroidManifest.xml @@ -20,6 +20,7 @@ + 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 7d3d4fce74..855abf7f24 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,11 +17,11 @@ package com.android.cts.net.hostside; import android.content.Intent; +import android.content.pm.PackageManager.NameNotFoundException; import android.net.Network; 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; @@ -38,6 +38,9 @@ public class MyVpnService extends VpnService { public static final String ACTION_ESTABLISHED = "com.android.cts.net.hostside.ESTABNLISHED"; public static final String EXTRA_ALWAYS_ON = "is-always-on"; public static final String EXTRA_LOCKDOWN_ENABLED = "is-lockdown-enabled"; + public static final String CMD_CONNECT = "connect"; + public static final String CMD_DISCONNECT = "disconnect"; + public static final String CMD_UPDATE_UNDERLYING_NETWORKS = "update_underlying_networks"; private ParcelFileDescriptor mFd = null; private PacketReflector mPacketReflector = null; @@ -46,15 +49,24 @@ public class MyVpnService extends VpnService { public int onStartCommand(Intent intent, int flags, int startId) { String packageName = getPackageName(); String cmd = intent.getStringExtra(packageName + ".cmd"); - if ("disconnect".equals(cmd)) { + if (CMD_DISCONNECT.equals(cmd)) { stop(); - } else if ("connect".equals(cmd)) { + } else if (CMD_CONNECT.equals(cmd)) { start(packageName, intent); + } else if (CMD_UPDATE_UNDERLYING_NETWORKS.equals(cmd)) { + updateUnderlyingNetworks(packageName, intent); } return START_NOT_STICKY; } + private void updateUnderlyingNetworks(String packageName, Intent intent) { + final ArrayList underlyingNetworks = + intent.getParcelableArrayListExtra(packageName + ".underlyingNetworks"); + setUnderlyingNetworks( + (underlyingNetworks != null) ? underlyingNetworks.toArray(new Network[0]) : null); + } + private void start(String packageName, Intent intent) { Builder builder = new Builder(); 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 3abc4fb0e3..65ea4b1fe3 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 @@ -17,6 +17,8 @@ package com.android.cts.net.hostside; import static android.Manifest.permission.NETWORK_SETTINGS; +import static android.content.pm.PackageManager.FEATURE_TELEPHONY; +import static android.content.pm.PackageManager.FEATURE_WIFI; import static android.net.ConnectivityManager.TYPE_VPN; import static android.net.NetworkCapabilities.TRANSPORT_VPN; import static android.os.Process.INVALID_UID; @@ -33,12 +35,14 @@ 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.testutils.Cleanup.testAndCleanup; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; import android.annotation.Nullable; import android.app.Activity; @@ -65,7 +69,9 @@ import android.net.Uri; import android.net.VpnManager; 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; @@ -80,6 +86,7 @@ import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; import android.system.StructPollfd; +import android.telephony.TelephonyManager; import android.test.MoreAsserts; import android.text.TextUtils; import android.util.Log; @@ -88,10 +95,14 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.android.compatibility.common.util.BlockingBroadcastReceiver; import com.android.modules.utils.build.SdkLevel; +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; +import com.android.testutils.RecorderCallback; import com.android.testutils.TestableNetworkCallback; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -151,6 +162,7 @@ public class VpnTest { private static final String PRIVATE_DNS_MODE_PROVIDER_HOSTNAME = "hostname"; private static final String PRIVATE_DNS_MODE_OPPORTUNISTIC = "opportunistic"; private static final String PRIVATE_DNS_SPECIFIER_SETTING = "private_dns_specifier"; + private static final int NETWORK_CALLBACK_TIMEOUT_MS = 30_000; public static String TAG = "VpnTest"; public static int TIMEOUT_MS = 3 * 1000; @@ -163,6 +175,9 @@ public class VpnTest { private ConnectivityManager mCM; private WifiManager mWifiManager; private RemoteSocketFactoryClient mRemoteSocketFactoryClient; + private CtsNetUtils mCtsNetUtils; + private PackageManager mPackageManager; + private TelephonyManager mTelephonyManager; Network mNetwork; NetworkCallback mCallback; @@ -172,6 +187,9 @@ public class VpnTest { private String mOldPrivateDnsMode; private String mOldPrivateDnsSpecifier; + @Rule + public final DevSdkIgnoreRule mDevSdkIgnoreRule = new DevSdkIgnoreRule(); + private boolean supportedHardware() { final PackageManager pm = getInstrumentation().getContext().getPackageManager(); return !pm.hasSystemFeature("android.hardware.type.watch"); @@ -201,6 +219,10 @@ public class VpnTest { mRemoteSocketFactoryClient = new RemoteSocketFactoryClient(mActivity); mRemoteSocketFactoryClient.bind(); mDevice.waitForIdle(); + mCtsNetUtils = new CtsNetUtils(getInstrumentation().getContext()); + mPackageManager = getInstrumentation().getContext().getPackageManager(); + mTelephonyManager = + getInstrumentation().getContext().getSystemService(TelephonyManager.class); } @After @@ -210,6 +232,7 @@ public class VpnTest { if (mCallback != null) { mCM.unregisterNetworkCallback(mCallback); } + mCtsNetUtils.tearDown(); Log.i(TAG, "Stopping VPN"); stopVpn(); mActivity.finish(); @@ -266,6 +289,32 @@ public class VpnTest { } } + private void updateUnderlyingNetworks(@Nullable ArrayList underlyingNetworks) + throws Exception { + final Intent intent = new Intent(mActivity, MyVpnService.class) + .putExtra(mPackageName + ".cmd", MyVpnService.CMD_UPDATE_UNDERLYING_NETWORKS) + .putParcelableArrayListExtra( + mPackageName + ".underlyingNetworks", underlyingNetworks); + mActivity.startService(intent); + } + + private void establishVpn(String[] addresses, String[] routes, String allowedApplications, + String disallowedApplications, @Nullable ProxyInfo proxyInfo, + @Nullable ArrayList underlyingNetworks, boolean isAlwaysMetered) + throws Exception { + final Intent intent = new Intent(mActivity, MyVpnService.class) + .putExtra(mPackageName + ".cmd", MyVpnService.CMD_CONNECT) + .putExtra(mPackageName + ".addresses", TextUtils.join(",", addresses)) + .putExtra(mPackageName + ".routes", TextUtils.join(",", routes)) + .putExtra(mPackageName + ".allowedapplications", allowedApplications) + .putExtra(mPackageName + ".disallowedapplications", disallowedApplications) + .putExtra(mPackageName + ".httpProxy", proxyInfo) + .putParcelableArrayListExtra( + mPackageName + ".underlyingNetworks", underlyingNetworks) + .putExtra(mPackageName + ".isAlwaysMetered", isAlwaysMetered); + mActivity.startService(intent); + } + // TODO: Consider replacing arguments with a Builder. private void startVpn( String[] addresses, String[] routes, String allowedApplications, @@ -291,18 +340,8 @@ public class VpnTest { 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) - .putExtra(mPackageName + ".httpProxy", proxyInfo) - .putParcelableArrayListExtra( - mPackageName + ".underlyingNetworks", underlyingNetworks) - .putExtra(mPackageName + ".isAlwaysMetered", isAlwaysMetered); - - mActivity.startService(intent); + establishVpn(addresses, routes, allowedApplications, disallowedApplications, proxyInfo, + underlyingNetworks, isAlwaysMetered); synchronized (mLock) { if (mNetwork == null) { Log.i(TAG, "bf mLock"); @@ -344,7 +383,7 @@ public class VpnTest { // 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"); + .putExtra(mPackageName + ".cmd", MyVpnService.CMD_DISCONNECT); mActivity.startService(intent); synchronized (mLockShutdown) { try { @@ -724,6 +763,83 @@ public class VpnTest { setAndVerifyPrivateDns(initialMode); } + private NetworkRequest makeVpnNetworkRequest() { + return new NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_VPN) + .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) + .build(); + } + + private void expectUnderlyingNetworks(TestableNetworkCallback callback, + @Nullable List expectUnderlyingNetworks) { + callback.eventuallyExpect(RecorderCallback.CallbackEntry.NETWORK_CAPS_UPDATED, + NETWORK_CALLBACK_TIMEOUT_MS, + entry -> (Objects.equals(expectUnderlyingNetworks, + ((RecorderCallback.CallbackEntry.CapabilitiesChanged) entry) + .getCaps().getUnderlyingNetworks()))); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.S) + public void testChangeUnderlyingNetworks() throws Exception { + assumeTrue(supportedHardware()); + assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI)); + assumeTrue(mPackageManager.hasSystemFeature(FEATURE_TELEPHONY)); + final TestableNetworkCallback callback = new TestableNetworkCallback(); + final boolean isWifiEnabled = mWifiManager.isWifiEnabled(); + testAndCleanup(() -> { + // Ensure both of wifi and mobile data are connected. + final Network wifiNetwork = mCtsNetUtils.ensureWifiConnected(); + assertTrue("Wifi is not connected", (wifiNetwork != null)); + final Network cellNetwork = mCtsNetUtils.connectToCell(); + assertTrue("Mobile data is not connected", (cellNetwork != null)); + // Store current default network. + final Network defaultNetwork = mCM.getActiveNetwork(); + // Start VPN and set empty array as its underlying networks. + startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"} /* addresses */, + new String[] {"0.0.0.0/0", "::/0"} /* routes */, + "" /* allowedApplications */, "" /* disallowedApplications */, + null /* proxyInfo */, new ArrayList<>() /* underlyingNetworks */, + false /* isAlwaysMetered */); + // Acquire the NETWORK_SETTINGS permission for getting the underlying networks. + runWithShellPermissionIdentity(() -> { + mCM.registerNetworkCallback(makeVpnNetworkRequest(), callback); + // Check that this VPN doesn't have any underlying networks. + expectUnderlyingNetworks(callback, new ArrayList()); + + // Update the underlying networks to null and the underlying networks should follow + // the system default network. + updateUnderlyingNetworks(null); + expectUnderlyingNetworks(callback, List.of(defaultNetwork)); + + // Update the underlying networks to mobile data. + updateUnderlyingNetworks(new ArrayList<>(List.of(cellNetwork))); + // Check the underlying networks of NetworkCapabilities which comes from + // onCapabilitiesChanged is mobile data. + expectUnderlyingNetworks(callback, List.of(cellNetwork)); + + // Update the underlying networks to wifi. + updateUnderlyingNetworks(new ArrayList<>(List.of(wifiNetwork))); + // Check the underlying networks of NetworkCapabilities which comes from + // onCapabilitiesChanged is wifi. + expectUnderlyingNetworks(callback, List.of(wifiNetwork)); + + // Update the underlying networks to wifi and mobile data. + updateUnderlyingNetworks(new ArrayList<>(List.of(wifiNetwork, cellNetwork))); + // Check the underlying networks of NetworkCapabilities which comes from + // onCapabilitiesChanged is wifi and mobile data. + expectUnderlyingNetworks(callback, List.of(wifiNetwork, cellNetwork)); + }, NETWORK_SETTINGS); + }, () -> { + if (isWifiEnabled) { + mCtsNetUtils.ensureWifiConnected(); + } else { + mCtsNetUtils.ensureWifiDisconnected(null); + } + }, () -> { + mCM.unregisterNetworkCallback(callback); + }); + } + @Test public void testDefault() throws Exception { if (!supportedHardware()) return; 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 49b5f9dc96..180015d433 100644 --- a/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java +++ b/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java @@ -33,6 +33,10 @@ public class HostsideVpnTests extends HostsideNetworkTestCase { uninstallPackage(TEST_APP2_PKG, true); } + public void testChangeUnderlyingNetworks() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testChangeUnderlyingNetworks"); + } + public void testDefault() throws Exception { runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testDefault"); } diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java index ce873f7f0b..7254319a52 100644 --- a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java +++ b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java @@ -380,6 +380,12 @@ public final class CtsNetUtils { return mCellNetworkCallback != null; } + public void tearDown() { + if (cellConnectAttempted()) { + disconnectFromCell(); + } + } + private NetworkRequest makeWifiNetworkRequest() { return new NetworkRequest.Builder() .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)