Merge "Add tests for VPN validation in NetworkMonitor"
diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
index 6e9f0cd..9fa146f 100644
--- a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
+++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
@@ -17,15 +17,16 @@
 package android.net.cts;
 
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
 
 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+import static com.android.testutils.TestableNetworkCallbackKt.anyNetwork;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -40,7 +41,6 @@
 import android.net.Ikev2VpnProfile;
 import android.net.IpSecAlgorithm;
 import android.net.Network;
-import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
 import android.net.ProxyInfo;
 import android.net.TestNetworkInterface;
@@ -53,8 +53,14 @@
 import androidx.test.InstrumentationRegistry;
 
 import com.android.internal.util.HexDump;
+import com.android.networkstack.apishim.Ikev2VpnProfileBuilderShimImpl;
+import com.android.networkstack.apishim.Ikev2VpnProfileShimImpl;
+import com.android.networkstack.apishim.common.Ikev2VpnProfileShim;
+import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.RecorderCallback.CallbackEntry;
+import com.android.testutils.TestableNetworkCallback;
 
 import org.bouncycastle.x509.X509V1CertificateGenerator;
 import org.junit.After;
@@ -167,6 +173,7 @@
     private static final VpnManager sVpnMgr =
             (VpnManager) sContext.getSystemService(Context.VPN_MANAGEMENT_SERVICE);
     private static final CtsNetUtils mCtsNetUtils = new CtsNetUtils(sContext);
+    private static final long TIMEOUT_MS = 15_000;
 
     private final X509Certificate mServerRootCa;
     private final CertificateAndKey mUserCertKey;
@@ -197,31 +204,32 @@
         }, Manifest.permission.MANAGE_TEST_NETWORKS);
     }
 
-    private Ikev2VpnProfile buildIkev2VpnProfileCommon(
-            Ikev2VpnProfile.Builder builder, boolean isRestrictedToTestNetworks) throws Exception {
+    private Ikev2VpnProfile buildIkev2VpnProfileCommon(@NonNull Ikev2VpnProfile.Builder builder,
+            boolean isRestrictedToTestNetworks,
+            boolean requiresValidation) throws Exception {
         if (isRestrictedToTestNetworks) {
             builder.restrictToTestNetworks();
         }
 
-        return builder.setBypassable(true)
+        builder.setBypassable(true)
                 .setAllowedAlgorithms(TEST_ALLOWED_ALGORITHMS)
                 .setProxy(TEST_PROXY_INFO)
                 .setMaxMtu(TEST_MTU)
-                .setMetered(false)
-                .build();
+                .setMetered(false);
+        if (TestUtils.shouldTestTApis()) {
+            Ikev2VpnProfileBuilderShimImpl.newInstance().setRequiresInternetValidation(
+                    builder, requiresValidation);
+        }
+        return builder.build();
     }
 
-    private Ikev2VpnProfile buildIkev2VpnProfilePsk(boolean isRestrictedToTestNetworks)
-            throws Exception {
-        return buildIkev2VpnProfilePsk(TEST_SERVER_ADDR_V6, isRestrictedToTestNetworks);
-    }
-
-    private Ikev2VpnProfile buildIkev2VpnProfilePsk(
-            String remote, boolean isRestrictedToTestNetworks) throws Exception {
+    private Ikev2VpnProfile buildIkev2VpnProfilePsk(@NonNull String remote,
+            boolean isRestrictedToTestNetworks, boolean requiresValidation) throws Exception {
         final Ikev2VpnProfile.Builder builder =
                 new Ikev2VpnProfile.Builder(remote, TEST_IDENTITY).setAuthPsk(TEST_PSK);
 
-        return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks);
+        return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
+                requiresValidation);
     }
 
     private Ikev2VpnProfile buildIkev2VpnProfileUsernamePassword(boolean isRestrictedToTestNetworks)
@@ -230,7 +238,8 @@
                 new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
                         .setAuthUsernamePassword(TEST_USER, TEST_PASSWORD, mServerRootCa);
 
-        return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks);
+        return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
+                false /* requiresValidation */);
     }
 
     private Ikev2VpnProfile buildIkev2VpnProfileDigitalSignature(boolean isRestrictedToTestNetworks)
@@ -240,7 +249,8 @@
                         .setAuthDigitalSignature(
                                 mUserCertKey.cert, mUserCertKey.key, mServerRootCa);
 
-        return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks);
+        return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
+                false /* requiresValidation */);
     }
 
     private void checkBasicIkev2VpnProfile(@NonNull Ikev2VpnProfile profile) throws Exception {
@@ -254,12 +264,11 @@
         assertFalse(profile.isRestrictedToTestNetworks());
     }
 
-    @Test
-    public void testBuildIkev2VpnProfilePsk() throws Exception {
+    public void doTestBuildIkev2VpnProfilePsk(final boolean requiresValidation) throws Exception {
         assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
 
-        final Ikev2VpnProfile profile =
-                buildIkev2VpnProfilePsk(false /* isRestrictedToTestNetworks */);
+        final Ikev2VpnProfile profile = buildIkev2VpnProfilePsk(TEST_SERVER_ADDR_V6,
+                false /* isRestrictedToTestNetworks */, requiresValidation);
 
         checkBasicIkev2VpnProfile(profile);
         assertArrayEquals(TEST_PSK, profile.getPresharedKey());
@@ -270,6 +279,22 @@
         assertNull(profile.getServerRootCaCert());
         assertNull(profile.getRsaPrivateKey());
         assertNull(profile.getUserCert());
+        final Ikev2VpnProfileShim<Ikev2VpnProfile> shim = Ikev2VpnProfileShimImpl.newInstance();
+        if (TestUtils.shouldTestTApis()) {
+            assertEquals(requiresValidation, shim.isInternetValidationRequired(profile));
+        } else {
+            try {
+                shim.isInternetValidationRequired(profile);
+                fail("Only supported from API level 33");
+            } catch (UnsupportedApiLevelException expected) {
+            }
+        }
+    }
+
+    @Test
+    public void testBuildIkev2VpnProfilePsk() throws Exception {
+        doTestBuildIkev2VpnProfilePsk(true /* requiresValidation */);
+        doTestBuildIkev2VpnProfilePsk(false /* requiresValidation */);
     }
 
     @Test
@@ -316,8 +341,8 @@
         setAppop(AppOpsManager.OP_ACTIVATE_VPN, hasActivateVpn);
         setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, hasActivatePlatformVpn);
 
-        final Ikev2VpnProfile profile =
-                buildIkev2VpnProfilePsk(false /* isRestrictedToTestNetworks */);
+        final Ikev2VpnProfile profile = buildIkev2VpnProfilePsk(TEST_SERVER_ADDR_V6,
+                false /* isRestrictedToTestNetworks */, false /* requiresValidation */);
         final Intent intent = sVpnMgr.provisionVpnProfile(profile);
         assertEquals(expectIntent, intent != null);
     }
@@ -360,8 +385,8 @@
 
         setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, true);
 
-        final Ikev2VpnProfile profile =
-                buildIkev2VpnProfilePsk(false /* isRestrictedToTestNetworks */);
+        final Ikev2VpnProfile profile = buildIkev2VpnProfilePsk(TEST_SERVER_ADDR_V6,
+                false /* isRestrictedToTestNetworks */, false /* requiresValidation */);
         assertNull(sVpnMgr.provisionVpnProfile(profile));
 
         // Verify that deleting the profile works (even without the appop)
@@ -394,7 +419,8 @@
         }
     }
 
-    private void checkStartStopVpnProfileBuildsNetworks(IkeTunUtils tunUtils, boolean testIpv6)
+    private void checkStartStopVpnProfileBuildsNetworks(@NonNull IkeTunUtils tunUtils,
+            boolean testIpv6, boolean requiresValidation)
             throws Exception {
         String serverAddr = testIpv6 ? TEST_SERVER_ADDR_V6 : TEST_SERVER_ADDR_V4;
         String initResp = testIpv6 ? SUCCESSFUL_IKE_INIT_RESP_V6 : SUCCESSFUL_IKE_INIT_RESP_V4;
@@ -404,10 +430,15 @@
         // Requires MANAGE_TEST_NETWORKS to provision a test-mode profile.
         mCtsNetUtils.setAppopPrivileged(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, true);
 
-        final Ikev2VpnProfile profile =
-                buildIkev2VpnProfilePsk(serverAddr, true /* isRestrictedToTestNetworks */);
+        final Ikev2VpnProfile profile = buildIkev2VpnProfilePsk(serverAddr,
+                true /* isRestrictedToTestNetworks */, requiresValidation);
         assertNull(sVpnMgr.provisionVpnProfile(profile));
 
+        final TestableNetworkCallback cb = new TestableNetworkCallback(TIMEOUT_MS);
+        final NetworkRequest nr = new NetworkRequest.Builder()
+                .clearCapabilities().addTransportType(TRANSPORT_VPN).build();
+        sCM.registerNetworkCallback(nr, cb);
+
         sVpnMgr.startProvisionedVpnProfile();
 
         // Inject IKE negotiation
@@ -418,35 +449,49 @@
                 HexDump.hexStringToByteArray(authResp));
 
         // Verify the VPN network came up
-        final NetworkRequest nr = new NetworkRequest.Builder()
-                .clearCapabilities().addTransportType(TRANSPORT_VPN).build();
+        final Network vpnNetwork = cb.expectCallback(CallbackEntry.AVAILABLE, anyNetwork())
+                .getNetwork();
 
-        final TestNetworkCallback cb = new TestNetworkCallback();
-        sCM.requestNetwork(nr, cb);
-        cb.waitForAvailable();
-        final Network vpnNetwork = cb.currentNetwork;
-        assertNotNull(vpnNetwork);
+        cb.expectCapabilitiesThat(vpnNetwork, TIMEOUT_MS, caps -> caps.hasTransport(TRANSPORT_VPN)
+                && caps.hasCapability(NET_CAPABILITY_INTERNET)
+                && !caps.hasCapability(NET_CAPABILITY_VALIDATED)
+                && Process.myUid() == caps.getOwnerUid());
+        cb.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, vpnNetwork);
+        cb.expectCallback(CallbackEntry.BLOCKED_STATUS, vpnNetwork);
 
-        final NetworkCapabilities caps = sCM.getNetworkCapabilities(vpnNetwork);
-        assertTrue(caps.hasTransport(TRANSPORT_VPN));
-        assertTrue(caps.hasCapability(NET_CAPABILITY_INTERNET));
-        assertEquals(Process.myUid(), caps.getOwnerUid());
+        // A VPN that requires validation is initially not validated, while one that doesn't
+        // immediately validate automatically. Because this VPN can't actually access Internet,
+        // the VPN only validates if it doesn't require validation. If the VPN requires validation
+        // but unexpectedly sends this callback, expecting LOST below will fail because the next
+        // callback will be the validated capabilities instead.
+        // In S and below, |requiresValidation| is ignored, so this callback is always sent
+        // regardless of its value.
+        if (!requiresValidation || !TestUtils.shouldTestTApis()) {
+            cb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED, TIMEOUT_MS,
+                    entry -> ((CallbackEntry.CapabilitiesChanged) entry).getCaps()
+                            .hasCapability(NET_CAPABILITY_VALIDATED));
+        }
 
         sVpnMgr.stopProvisionedVpnProfile();
-        cb.waitForLost();
-        assertEquals(vpnNetwork, cb.lastLostNetwork);
+        // Using expectCallback may cause the test to be flaky since test may receive other
+        // callbacks such as linkproperties change.
+        cb.eventuallyExpect(CallbackEntry.LOST, TIMEOUT_MS,
+                lost -> vpnNetwork.equals(lost.getNetwork()));
     }
 
     private class VerifyStartStopVpnProfileTest implements TestNetworkRunnable.Test {
         private final boolean mTestIpv6Only;
+        private final boolean mRequiresValidation;
 
         /**
          * Constructs the test
          *
          * @param testIpv6Only if true, builds a IPv6-only test; otherwise builds a IPv4-only test
+         * @param requiresValidation whether this VPN should request platform validation
          */
-        VerifyStartStopVpnProfileTest(boolean testIpv6Only) {
+        VerifyStartStopVpnProfileTest(boolean testIpv6Only, boolean requiresValidation) {
             mTestIpv6Only = testIpv6Only;
+            mRequiresValidation = requiresValidation;
         }
 
         @Override
@@ -454,7 +499,8 @@
                 throws Exception {
             final IkeTunUtils tunUtils = new IkeTunUtils(testIface.getFileDescriptor());
 
-            checkStartStopVpnProfileBuildsNetworks(tunUtils, mTestIpv6Only);
+            checkStartStopVpnProfileBuildsNetworks(
+                    tunUtils, mTestIpv6Only, mRequiresValidation);
         }
 
         @Override
@@ -478,7 +524,10 @@
 
         // Requires shell permission to update appops.
         runWithShellPermissionIdentity(
-                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(false)));
+                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(false, false)));
+
+        runWithShellPermissionIdentity(
+                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(false, true)));
     }
 
     @Test
@@ -487,7 +536,9 @@
 
         // Requires shell permission to update appops.
         runWithShellPermissionIdentity(
-                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(true)));
+                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(true, false)));
+        runWithShellPermissionIdentity(
+                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(true, true)));
     }
 
     private static class CertificateAndKey {
diff --git a/tests/cts/net/src/android/net/cts/TestUtils.java b/tests/cts/net/src/android/net/cts/TestUtils.java
index c1100b1..001aa01 100644
--- a/tests/cts/net/src/android/net/cts/TestUtils.java
+++ b/tests/cts/net/src/android/net/cts/TestUtils.java
@@ -16,6 +16,8 @@
 
 package android.net.cts;
 
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
 import android.os.Build;
 
 import com.android.modules.utils.build.SdkLevel;
@@ -33,4 +35,13 @@
     public static boolean shouldTestSApis() {
         return SdkLevel.isAtLeastS() && ConstantsShim.VERSION > Build.VERSION_CODES.R;
     }
+
+    /**
+     * Whether to test T+ APIs. This requires a) that the test be running on an S+ device, and
+     * b) that the code be compiled against shims new enough to access these APIs.
+     */
+    public static boolean shouldTestTApis() {
+        // TODO: replace SC_V2 with Build.VERSION_CODES.S_V2 when it's available in mainline branch.
+        return SdkLevel.isAtLeastT() && ConstantsShim.VERSION > SC_V2;
+    }
 }
diff --git a/tests/integration/util/com/android/server/NetworkAgentWrapper.java b/tests/integration/util/com/android/server/NetworkAgentWrapper.java
index 365c0cf..2763f5a 100644
--- a/tests/integration/util/com/android/server/NetworkAgentWrapper.java
+++ b/tests/integration/util/com/android/server/NetworkAgentWrapper.java
@@ -343,6 +343,10 @@
         return mNetworkAgent;
     }
 
+    public NetworkAgentConfig getNetworkAgentConfig() {
+        return mNetworkAgentConfig;
+    }
+
     public NetworkCapabilities getNetworkCapabilities() {
         return mNetworkCapabilities;
     }
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 025b28c..4c76803 100644
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -337,6 +337,7 @@
 import com.android.net.module.util.ArrayTrackRecord;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.LocationPermissionChecker;
+import com.android.networkstack.apishim.NetworkAgentConfigShimImpl;
 import com.android.server.ConnectivityService.ConnectivityDiagnosticsCallbackInfo;
 import com.android.server.ConnectivityService.NetworkRequestInfo;
 import com.android.server.ConnectivityServiceTest.ConnectivityServiceDependencies.ReportedInterfaces;
@@ -1373,6 +1374,10 @@
             return (mMockNetworkAgent == null) ? null : mMockNetworkAgent.getNetwork();
         }
 
+        public NetworkAgentConfig getNetworkAgentConfig() {
+            return null == mMockNetworkAgent ? null : mMockNetworkAgent.getNetworkAgentConfig();
+        }
+
         @Override
         public int getActiveVpnType() {
             return mVpnType;
@@ -2936,6 +2941,7 @@
     @Test
     public void testRequiresValidation() {
         assertTrue(NetworkMonitorUtils.isValidationRequired(
+                NetworkAgentConfigShimImpl.newInstance(null),
                 mCm.getDefaultRequest().networkCapabilities));
     }
 
@@ -7933,6 +7939,7 @@
         // VPN networks do not satisfy the default request and are automatically validated
         // by NetworkMonitor
         assertFalse(NetworkMonitorUtils.isValidationRequired(
+                NetworkAgentConfigShimImpl.newInstance(mMockVpn.getNetworkAgentConfig()),
                 mMockVpn.getAgent().getNetworkCapabilities()));
         mMockVpn.getAgent().setNetworkValid(false /* isStrictMode */);
 
@@ -8083,6 +8090,7 @@
         assertTrue(nc.hasCapability(NET_CAPABILITY_INTERNET));
 
         assertFalse(NetworkMonitorUtils.isValidationRequired(
+                NetworkAgentConfigShimImpl.newInstance(mMockVpn.getNetworkAgentConfig()),
                 mMockVpn.getAgent().getNetworkCapabilities()));
         assertTrue(NetworkMonitorUtils.isPrivateDnsValidationRequired(
                 mMockVpn.getAgent().getNetworkCapabilities()));