diff --git a/tests/cts/hostside/TEST_MAPPING b/tests/cts/hostside/TEST_MAPPING index ab6de828e1..2cfd7af1ae 100644 --- a/tests/cts/hostside/TEST_MAPPING +++ b/tests/cts/hostside/TEST_MAPPING @@ -8,6 +8,9 @@ }, { "exclude-annotation": "android.platform.test.annotations.FlakyTest" + }, + { + "exclude-annotation": "android.platform.test.annotations.RequiresDevice" } ] } 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 10845d7112..b6902b5fe3 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 @@ -18,6 +18,8 @@ package com.android.cts.net.hostside; import static android.Manifest.permission.MANAGE_TEST_NETWORKS; import static android.Manifest.permission.NETWORK_SETTINGS; +import static android.Manifest.permission.READ_DEVICE_CONFIG; +import static android.Manifest.permission.WRITE_DEVICE_CONFIG; import static android.content.pm.PackageManager.FEATURE_TELEPHONY; import static android.content.pm.PackageManager.FEATURE_WIFI; import static android.net.ConnectivityManager.TYPE_VPN; @@ -36,6 +38,12 @@ 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.cts.net.hostside.VpnTest.TestSocketKeepaliveCallback.CallbackType.ON_DATA_RECEIVED; +import static com.android.cts.net.hostside.VpnTest.TestSocketKeepaliveCallback.CallbackType.ON_ERROR; +import static com.android.cts.net.hostside.VpnTest.TestSocketKeepaliveCallback.CallbackType.ON_PAUSED; +import static com.android.cts.net.hostside.VpnTest.TestSocketKeepaliveCallback.CallbackType.ON_RESUMED; +import static com.android.cts.net.hostside.VpnTest.TestSocketKeepaliveCallback.CallbackType.ON_STARTED; +import static com.android.cts.net.hostside.VpnTest.TestSocketKeepaliveCallback.CallbackType.ON_STOPPED; 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.networkstack.apishim.ConstantsShim.RECEIVER_EXPORTED; @@ -64,6 +72,7 @@ import android.content.pm.PackageManager; import android.database.Cursor; import android.net.ConnectivityManager; import android.net.ConnectivityManager.NetworkCallback; +import android.net.IpSecManager; import android.net.LinkAddress; import android.net.LinkProperties; import android.net.Network; @@ -71,6 +80,7 @@ import android.net.NetworkCapabilities; import android.net.NetworkRequest; import android.net.Proxy; import android.net.ProxyInfo; +import android.net.SocketKeepalive; import android.net.TestNetworkInterface; import android.net.TestNetworkManager; import android.net.TransportInfo; @@ -79,6 +89,7 @@ import android.net.VpnManager; import android.net.VpnService; import android.net.VpnTransportInfo; import android.net.cts.util.CtsNetUtils; +import android.net.util.KeepaliveUtils; import android.net.wifi.WifiManager; import android.os.Build; import android.os.Handler; @@ -87,6 +98,7 @@ import android.os.ParcelFileDescriptor; import android.os.Process; import android.os.SystemProperties; import android.os.UserHandle; +import android.provider.DeviceConfig; import android.provider.Settings; import android.support.test.uiautomator.UiDevice; import android.support.test.uiautomator.UiObject; @@ -105,6 +117,8 @@ 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.ArrayTrackRecord; +import com.android.net.module.util.CollectionUtils; import com.android.net.module.util.PacketBuilder; import com.android.testutils.DevSdkIgnoreRule; import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; @@ -141,6 +155,7 @@ import java.util.Random; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; /** @@ -190,6 +205,12 @@ public class VpnTest { public static int SOCKET_TIMEOUT_MS = 100; public static String TEST_HOST = "connectivitycheck.gstatic.com"; + private static final String AUTOMATIC_ON_OFF_KEEPALIVE_VERSION = + "automatic_on_off_keepalive_version"; + // Enabled since version 1 means it's always enabled because the version is always above 1 + private static final String AUTOMATIC_ON_OFF_KEEPALIVE_ENABLED = "1"; + private static final long TEST_TCP_POLLING_TIMER_EXPIRED_PERIOD_MS = 60_000L; + private UiDevice mDevice; private MyActivity mActivity; private String mPackageName; @@ -1041,6 +1062,183 @@ public class VpnTest { checkStrictModePrivateDns(); } + private int getSupportedKeepalives(NetworkCapabilities nc) throws Exception { + // Get number of supported concurrent keepalives for testing network. + final int[] keepalivesPerTransport = KeepaliveUtils.getSupportedKeepalives( + mTargetContext); + return KeepaliveUtils.getSupportedKeepalivesForNetworkCapabilities( + keepalivesPerTransport, nc); + } + + // This class can't be private, otherwise the constants can't be static imported. + static class TestSocketKeepaliveCallback extends SocketKeepalive.Callback { + // This must be larger than the alarm delay in AutomaticOnOffKeepaliveTracker. + private static final int KEEPALIVE_TIMEOUT_MS = 10_000; + public enum CallbackType { + ON_STARTED, + ON_RESUMED, + ON_STOPPED, + ON_PAUSED, + ON_ERROR, + ON_DATA_RECEIVED + } + private ArrayTrackRecord mHistory = new ArrayTrackRecord<>(); + private ArrayTrackRecord.ReadHead mEvents = mHistory.newReadHead(); + + @Override + public void onStarted() { + mHistory.add(ON_STARTED); + } + + @Override + public void onResumed() { + mHistory.add(ON_RESUMED); + } + + @Override + public void onStopped() { + mHistory.add(ON_STOPPED); + } + + @Override + public void onPaused() { + mHistory.add(ON_PAUSED); + } + + @Override + public void onError(final int error) { + mHistory.add(ON_ERROR); + } + + @Override + public void onDataReceived() { + mHistory.add(ON_DATA_RECEIVED); + } + + public CallbackType poll() { + return mEvents.poll(KEEPALIVE_TIMEOUT_MS, it -> true); + } + } + + private InetAddress getV4AddrByName(final String hostname) throws Exception { + final InetAddress[] allAddrs = InetAddress.getAllByName(hostname); + for (InetAddress addr : allAddrs) { + if (addr instanceof Inet4Address) return addr; + } + return null; + } + + @Test + public void testAutomaticOnOffKeepaliveModeNoClose() throws Exception { + doTestAutomaticOnOffKeepaliveMode(false); + } + + @Test + public void testAutomaticOnOffKeepaliveModeClose() throws Exception { + doTestAutomaticOnOffKeepaliveMode(true); + } + + private void startKeepalive(SocketKeepalive kp, TestSocketKeepaliveCallback callback) { + runWithShellPermissionIdentity(() -> { + // Only SocketKeepalive.start() requires READ_DEVICE_CONFIG because feature is protected + // by a feature flag. But also verify ON_STARTED callback received here to ensure + // keepalive is indeed started because start() runs in the executor thread and shell + // permission may be dropped before reading DeviceConfig. + kp.start(10 /* intervalSec */, SocketKeepalive.FLAG_AUTOMATIC_ON_OFF, mNetwork); + + // Verify callback status. + assertEquals(ON_STARTED, callback.poll()); + }, READ_DEVICE_CONFIG); + } + + private void doTestAutomaticOnOffKeepaliveMode(final boolean closeSocket) throws Exception { + assumeTrue(supportedHardware()); + + // Get default network first before starting VPN + final Network defaultNetwork = mCM.getActiveNetwork(); + final TestableNetworkCallback cb = new TestableNetworkCallback(); + registerDefaultNetworkCallback(cb); + cb.expect(CallbackEntry.AVAILABLE, defaultNetwork); + final NetworkCapabilities cap = + cb.expect(CallbackEntry.NETWORK_CAPS_UPDATED, defaultNetwork).getCaps(); + final LinkProperties lp = + cb.expect(CallbackEntry.LINK_PROPERTIES_CHANGED, defaultNetwork).getLp(); + cb.expect(CallbackEntry.BLOCKED_STATUS, defaultNetwork); + + // Setup VPN + final FileDescriptor fd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS); + final String allowedApps = mRemoteSocketFactoryClient.getPackageName() + "," + mPackageName; + startVpn(new String[]{"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, + new String[]{"192.0.2.0/24", "2001:db8::/32"}, + allowedApps, "" /* disallowedApplications */, null /* proxyInfo */, + null /* underlyingNetworks */, false /* isAlwaysMetered */); + assertSocketClosed(fd, TEST_HOST); + + // Decrease the TCP polling timer for testing. + runWithShellPermissionIdentity(() -> mCM.setTestLowTcpPollingTimerForKeepalive( + System.currentTimeMillis() + TEST_TCP_POLLING_TIMER_EXPIRED_PERIOD_MS), + NETWORK_SETTINGS); + + // Setup keepalive + final int supported = getSupportedKeepalives(cap); + assumeTrue("Network " + defaultNetwork + " does not support keepalive", supported != 0); + final InetAddress srcAddr = CollectionUtils.findFirst(lp.getAddresses(), + it -> it instanceof Inet4Address); + assumeTrue("This test requires native IPv4", srcAddr != null); + + final TestSocketKeepaliveCallback callback = new TestSocketKeepaliveCallback(); + + final String origMode = runWithShellPermissionIdentity(() -> { + final String mode = DeviceConfig.getProperty( + DeviceConfig.NAMESPACE_CONNECTIVITY, AUTOMATIC_ON_OFF_KEEPALIVE_VERSION); + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_CONNECTIVITY, + AUTOMATIC_ON_OFF_KEEPALIVE_VERSION, + AUTOMATIC_ON_OFF_KEEPALIVE_ENABLED, false /* makeDefault */); + return mode; + }, READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG); + + final IpSecManager ipSec = mTargetContext.getSystemService(IpSecManager.class); + SocketKeepalive kp = null; + try (IpSecManager.UdpEncapsulationSocket nattSocket = ipSec.openUdpEncapsulationSocket()) { + final InetAddress dstAddr = getV4AddrByName(TEST_HOST); + assertNotNull(dstAddr); + + // Start keepalive with dynamic keepalive mode enabled. + final Executor executor = mTargetContext.getMainExecutor(); + kp = mCM.createSocketKeepalive(defaultNetwork, nattSocket, + srcAddr, dstAddr, executor, callback); + startKeepalive(kp, callback); + + // There should be no open sockets on the VPN network, because any + // open sockets were closed when startVpn above was called. So the + // first TCP poll should trigger ON_PAUSED. + assertEquals(ON_PAUSED, callback.poll()); + + final Socket s = new Socket(); + mNetwork.bindSocket(s); + s.connect(new InetSocketAddress(dstAddr, 80)); + assertEquals(ON_RESUMED, callback.poll()); + + if (closeSocket) { + s.close(); + assertEquals(ON_PAUSED, callback.poll()); + } + + kp.stop(); + assertEquals(ON_STOPPED, callback.poll()); + } finally { + if (kp != null) kp.stop(); + + runWithShellPermissionIdentity(() -> { + DeviceConfig.setProperty( + DeviceConfig.NAMESPACE_CONNECTIVITY, + AUTOMATIC_ON_OFF_KEEPALIVE_VERSION, + origMode, false); + mCM.setTestLowTcpPollingTimerForKeepalive(0); + }, WRITE_DEVICE_CONFIG, NETWORK_SETTINGS); + } + } + @Test public void testAppDisallowed() throws Exception { assumeTrue(supportedHardware()); 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 10a2821e4c..603779d8fc 100644 --- a/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java +++ b/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java @@ -16,6 +16,8 @@ package com.android.cts.net; +import android.platform.test.annotations.RequiresDevice; + public class HostsideVpnTests extends HostsideNetworkTestCase { @Override @@ -89,6 +91,18 @@ public class HostsideVpnTests extends HostsideNetworkTestCase { TEST_PKG, TEST_PKG + ".VpnTest", "testAlwaysMeteredVpnWithNullUnderlyingNetwork"); } + @RequiresDevice // Keepalive is not supported on virtual hardware + public void testAutomaticOnOffKeepaliveModeClose() throws Exception { + runDeviceTests( + TEST_PKG, TEST_PKG + ".VpnTest", "testAutomaticOnOffKeepaliveModeClose"); + } + + @RequiresDevice // Keepalive is not supported on virtual hardware + public void testAutomaticOnOffKeepaliveModeNoClose() throws Exception { + runDeviceTests( + TEST_PKG, TEST_PKG + ".VpnTest", "testAutomaticOnOffKeepaliveModeNoClose"); + } + public void testAlwaysMeteredVpnWithNonNullUnderlyingNetwork() throws Exception { runDeviceTests( TEST_PKG,