Merge "[DK4] Add cts to test automatic on/off keepalive"
diff --git a/tests/cts/hostside/TEST_MAPPING b/tests/cts/hostside/TEST_MAPPING
index ab6de82..2cfd7af 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 10845d7..b6902b5 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 @@
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 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.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.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.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.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 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.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 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 @@
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<CallbackType> mHistory = new ArrayTrackRecord<>();
+ private ArrayTrackRecord<CallbackType>.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 10a2821..603779d 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 @@
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,