diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java index 37df4eb1ab..454381728e 100644 --- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java +++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java @@ -338,6 +338,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.networkstack.apishim.api29.ConstantsShim; import com.android.server.ConnectivityService.ConnectivityDiagnosticsCallbackInfo; import com.android.server.ConnectivityService.NetworkRequestInfo; import com.android.server.ConnectivityServiceTest.ConnectivityServiceDependencies.ReportedInterfaces; @@ -644,7 +645,8 @@ public class ConnectivityServiceTest { @Override public ComponentName startService(Intent service) { final String action = service.getAction(); - if (!VpnConfig.SERVICE_INTERFACE.equals(action)) { + if (!VpnConfig.SERVICE_INTERFACE.equals(action) + && !ConstantsShim.ACTION_VPN_MANAGER_EVENT.equals(action)) { fail("Attempt to start unknown service, action=" + action); } return new ComponentName(service.getPackage(), "com.android.test.Service"); diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java index 33c0868c17..6da4f0729f 100644 --- a/tests/unit/java/com/android/server/connectivity/VpnTest.java +++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java @@ -27,6 +27,7 @@ import static android.content.pm.UserInfo.FLAG_RESTRICTED; import static android.net.ConnectivityManager.NetworkCallback; import static android.net.INetd.IF_STATE_DOWN; import static android.net.INetd.IF_STATE_UP; +import static android.net.VpnManager.TYPE_VPN_PLATFORM; import static android.os.UserHandle.PER_USER_RANGE; import static com.android.modules.utils.build.SdkLevel.isAtLeastT; @@ -54,6 +55,7 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -65,6 +67,7 @@ import android.app.AppOpsManager; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; +import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; @@ -88,10 +91,15 @@ import android.net.NetworkInfo.DetailedState; import android.net.RouteInfo; import android.net.UidRangeParcel; import android.net.VpnManager; +import android.net.VpnProfileState; import android.net.VpnService; import android.net.VpnTransportInfo; import android.net.ipsec.ike.IkeSessionCallback; +import android.net.ipsec.ike.exceptions.IkeException; +import android.net.ipsec.ike.exceptions.IkeNetworkLostException; +import android.net.ipsec.ike.exceptions.IkeNonProtocolException; import android.net.ipsec.ike.exceptions.IkeProtocolException; +import android.net.ipsec.ike.exceptions.IkeTimeoutException; import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.os.ConditionVariable; @@ -118,6 +126,7 @@ import com.android.server.IpSecService; import com.android.testutils.DevSdkIgnoreRule; import com.android.testutils.DevSdkIgnoreRunner; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -135,6 +144,7 @@ import java.io.FileWriter; import java.io.IOException; import java.net.Inet4Address; import java.net.InetAddress; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -271,6 +281,11 @@ public class VpnTest { doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(any()); } + @After + public void tearDown() throws Exception { + doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN); + } + private void mockService(Class clazz, String name, T service) { doReturn(service).when(mContext).getSystemService(name); doReturn(name).when(mContext).getSystemServiceName(clazz); @@ -783,6 +798,30 @@ public class VpnTest { verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG))); } + private void verifyPlatformVpnIsActivated(String packageName) { + verify(mAppOps).noteOpNoThrow( + eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN), + eq(Process.myUid()), + eq(packageName), + eq(null) /* attributionTag */, + eq(null) /* message */); + verify(mAppOps).startOp( + eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER), + eq(Process.myUid()), + eq(packageName), + eq(null) /* attributionTag */, + eq(null) /* message */); + } + + private void verifyPlatformVpnIsDeactivated(String packageName) { + // Add a small delay to double confirm that finishOp is only called once. + verify(mAppOps, after(100)).finishOp( + eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER), + eq(Process.myUid()), + eq(packageName), + eq(null) /* attributionTag */); + } + @Test public void testStartVpnProfile() throws Exception { final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); @@ -793,13 +832,7 @@ public class VpnTest { vpn.startVpnProfile(TEST_VPN_PKG); verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG))); - verify(mAppOps) - .noteOpNoThrow( - eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN), - eq(Process.myUid()), - eq(TEST_VPN_PKG), - eq(null) /* attributionTag */, - eq(null) /* message */); + verifyPlatformVpnIsActivated(TEST_VPN_PKG); } @Test @@ -811,7 +844,7 @@ public class VpnTest { vpn.startVpnProfile(TEST_VPN_PKG); - // Verify that the the ACTIVATE_VPN appop was checked, but no error was thrown. + // Verify that the ACTIVATE_VPN appop was checked, but no error was thrown. verify(mAppOps).noteOpNoThrow(AppOpsManager.OPSTR_ACTIVATE_VPN, Process.myUid(), TEST_VPN_PKG, null /* attributionTag */, null /* message */); } @@ -896,18 +929,7 @@ public class VpnTest { when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) .thenReturn(mVpnProfile.encode()); vpn.startVpnProfile(TEST_VPN_PKG); - verify(mAppOps).noteOpNoThrow( - eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN), - eq(Process.myUid()), - eq(TEST_VPN_PKG), - eq(null) /* attributionTag */, - eq(null) /* message */); - verify(mAppOps).startOp( - eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER), - eq(Process.myUid()), - eq(TEST_VPN_PKG), - eq(null) /* attributionTag */, - eq(null) /* message */); + verifyPlatformVpnIsActivated(TEST_VPN_PKG); // Add a small delay to make sure that startOp is only called once. verify(mAppOps, after(100).times(1)).startOp( eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER), @@ -923,12 +945,7 @@ public class VpnTest { eq(null) /* attributionTag */, eq(null) /* message */); vpn.stopVpnProfile(TEST_VPN_PKG); - // Add a small delay to double confirm that startOp is only called once. - verify(mAppOps, after(100)).finishOp( - eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER), - eq(Process.myUid()), - eq(TEST_VPN_PKG), - eq(null) /* attributionTag */); + verifyPlatformVpnIsDeactivated(TEST_VPN_PKG); } @Test @@ -964,6 +981,128 @@ public class VpnTest { eq(null) /* message */); } + private void verifyVpnManagerEvent(String sessionKey, String category, int errorClass, + int errorCode, VpnProfileState... profileState) { + final Context userContext = + mContext.createContextAsUser(UserHandle.of(primaryUser.id), 0 /* flags */); + final ArgumentCaptor intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class); + + final int verifyTimes = (profileState == null) ? 1 : profileState.length; + verify(userContext, times(verifyTimes)).startService(intentArgumentCaptor.capture()); + + for (int i = 0; i < verifyTimes; i++) { + final Intent intent = intentArgumentCaptor.getAllValues().get(i); + assertEquals(sessionKey, intent.getStringExtra(VpnManager.EXTRA_SESSION_KEY)); + final Set categories = intent.getCategories(); + assertTrue(categories.contains(category)); + assertEquals(errorClass, + intent.getIntExtra(VpnManager.EXTRA_ERROR_CLASS, -1 /* defaultValue */)); + assertEquals(errorCode, + intent.getIntExtra(VpnManager.EXTRA_ERROR_CODE, -1 /* defaultValue */)); + if (profileState != null) { + assertEquals(profileState[i], intent.getParcelableExtra( + VpnManager.EXTRA_VPN_PROFILE_STATE, VpnProfileState.class)); + } + } + reset(userContext); + } + + @Test + public void testVpnManagerEventForUserDeactivated() throws Exception { + assumeTrue(SdkLevel.isAtLeastT()); + // For security reasons, Vpn#prepare() will check that oldPackage and newPackage are either + // null or the package of the caller. This test will call Vpn#prepare() to pretend the old + // VPN is replaced by a new one. But only Settings can change to some other packages, and + // this is checked with CONTROL_VPN so simulate holding CONTROL_VPN in order to pass the + // security checks. + doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN); + final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) + .thenReturn(mVpnProfile.encode()); + + // Test the case that the user deactivates the vpn in vpn app. + final String sessionKey1 = vpn.startVpnProfile(TEST_VPN_PKG); + verifyPlatformVpnIsActivated(TEST_VPN_PKG); + vpn.stopVpnProfile(TEST_VPN_PKG); + verifyPlatformVpnIsDeactivated(TEST_VPN_PKG); + // CATEGORY_EVENT_DEACTIVATED_BY_USER is not an error event, so both of errorClass and + // errorCode won't be set. + verifyVpnManagerEvent(sessionKey1, VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER, + -1 /* errorClass */, -1 /* errorCode */, null /* profileState */); + reset(mAppOps); + + // Test the case that the user chooses another vpn and the original one is replaced. + final String sessionKey2 = vpn.startVpnProfile(TEST_VPN_PKG); + verifyPlatformVpnIsActivated(TEST_VPN_PKG); + vpn.prepare(TEST_VPN_PKG, "com.new.vpn" /* newPackage */, TYPE_VPN_PLATFORM); + verifyPlatformVpnIsDeactivated(TEST_VPN_PKG); + // CATEGORY_EVENT_DEACTIVATED_BY_USER is not an error event, so both of errorClass and + // errorCode won't be set. + verifyVpnManagerEvent(sessionKey2, VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER, + -1 /* errorClass */, -1 /* errorCode */, null /* profileState */); + } + + @Test + public void testVpnManagerEventForAlwaysOnChanged() throws Exception { + assumeTrue(SdkLevel.isAtLeastT()); + // Calling setAlwaysOnPackage() needs to hold CONTROL_VPN. + doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN); + final Vpn vpn = createVpn(primaryUser.id); + // Enable VPN always-on for PKGS[1]. + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false /* lockdown */, + null /* lockdownAllowlist */)); + verifyVpnManagerEvent(null /* sessionKey */, + VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */, + -1 /* errorCode */, new VpnProfileState(VpnProfileState.STATE_DISCONNECTED, + null /* sessionKey */, true /* alwaysOn */, false /* lockdown */)); + + // Enable VPN lockdown for PKGS[1]. + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true /* lockdown */, + null /* lockdownAllowlist */)); + verifyVpnManagerEvent(null /* sessionKey */, + VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */, + -1 /* errorCode */, new VpnProfileState(VpnProfileState.STATE_DISCONNECTED, + null /* sessionKey */, true /* alwaysOn */, true /* lockdown */)); + + // Disable VPN lockdown for PKGS[1]. + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false /* lockdown */, + null /* lockdownAllowlist */)); + verifyVpnManagerEvent(null /* sessionKey */, + VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */, + -1 /* errorCode */, new VpnProfileState(VpnProfileState.STATE_DISCONNECTED, + null /* sessionKey */, true /* alwaysOn */, false /* lockdown */)); + + // Disable VPN always-on. + assertTrue(vpn.setAlwaysOnPackage(null, false /* lockdown */, + null /* lockdownAllowlist */)); + verifyVpnManagerEvent(null /* sessionKey */, + VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */, + -1 /* errorCode */, new VpnProfileState(VpnProfileState.STATE_DISCONNECTED, + null /* sessionKey */, false /* alwaysOn */, false /* lockdown */)); + + // Enable VPN always-on for PKGS[1] again. + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false /* lockdown */, + null /* lockdownAllowlist */)); + verifyVpnManagerEvent(null /* sessionKey */, + VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */, + -1 /* errorCode */, new VpnProfileState(VpnProfileState.STATE_DISCONNECTED, + null /* sessionKey */, true /* alwaysOn */, false /* lockdown */)); + + // Enable VPN always-on for PKGS[2]. + assertTrue(vpn.setAlwaysOnPackage(PKGS[2], false /* lockdown */, + null /* lockdownAllowlist */)); + // PKGS[1] is replaced with PKGS[2]. + // Pass 2 VpnProfileState objects to verifyVpnManagerEvent(), the first one is sent to + // PKGS[1] to notify PKGS[1] that the VPN always-on is disabled, the second one is sent to + // PKGS[2] to notify PKGS[2] that the VPN always-on is enabled. + verifyVpnManagerEvent(null /* sessionKey */, + VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */, + -1 /* errorCode */, new VpnProfileState(VpnProfileState.STATE_DISCONNECTED, + null /* sessionKey */, false /* alwaysOn */, false /* lockdown */), + new VpnProfileState(VpnProfileState.STATE_DISCONNECTED, + null /* sessionKey */, true /* alwaysOn */, false /* lockdown */)); + } + @Test public void testSetPackageAuthorizationVpnService() throws Exception { final Vpn vpn = createVpnAndSetupUidChecks(); @@ -981,7 +1120,7 @@ public class VpnTest { public void testSetPackageAuthorizationPlatformVpn() throws Exception { final Vpn vpn = createVpnAndSetupUidChecks(); - assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, VpnManager.TYPE_VPN_PLATFORM)); + assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, TYPE_VPN_PLATFORM)); verify(mAppOps) .setMode( eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN), @@ -1031,15 +1170,16 @@ public class VpnTest { config -> Arrays.asList(config.flags).contains(flag))); } - @Test - public void testStartPlatformVpnAuthenticationFailed() throws Exception { + private void setupPlatformVpnWithSpecificExceptionAndItsErrorCode(IkeException exception, + String category, int errorType, int errorCode) throws Exception { final ArgumentCaptor captor = ArgumentCaptor.forClass(IkeSessionCallback.class); - final IkeProtocolException exception = mock(IkeProtocolException.class); - when(exception.getErrorType()) - .thenReturn(IkeProtocolException.ERROR_TYPE_AUTHENTICATION_FAILED); - final Vpn vpn = startLegacyVpn(createVpn(primaryUser.id), (mVpnProfile)); + final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) + .thenReturn(mVpnProfile.encode()); + + final String sessionKey = vpn.startVpnProfile(TEST_VPN_PKG); final NetworkCallback cb = triggerOnAvailableAndGetCallback(); verifyInterfaceSetCfgWithFlags(IF_STATE_UP); @@ -1049,10 +1189,75 @@ public class VpnTest { verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS)) .createIkeSession(any(), any(), any(), any(), captor.capture(), any()); final IkeSessionCallback ikeCb = captor.getValue(); - ikeCb.onClosedExceptionally(exception); + ikeCb.onClosedWithException(exception); - verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS)).unregisterNetworkCallback(eq(cb)); - assertEquals(LegacyVpnInfo.STATE_FAILED, vpn.getLegacyVpnInfo().state); + verifyVpnManagerEvent(sessionKey, category, errorType, errorCode, null /* profileState */); + if (errorType == VpnManager.ERROR_CLASS_NOT_RECOVERABLE) { + verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS)) + .unregisterNetworkCallback(eq(cb)); + } + } + + @Test + public void testStartPlatformVpnAuthenticationFailed() throws Exception { + final IkeProtocolException exception = mock(IkeProtocolException.class); + final int errorCode = IkeProtocolException.ERROR_TYPE_AUTHENTICATION_FAILED; + when(exception.getErrorType()).thenReturn(errorCode); + setupPlatformVpnWithSpecificExceptionAndItsErrorCode(exception, + VpnManager.CATEGORY_EVENT_IKE_ERROR, VpnManager.ERROR_CLASS_NOT_RECOVERABLE, + errorCode); + } + + @Test + public void testStartPlatformVpnFailedWithRecoverableError() throws Exception { + final IkeProtocolException exception = mock(IkeProtocolException.class); + final int errorCode = IkeProtocolException.ERROR_TYPE_TEMPORARY_FAILURE; + when(exception.getErrorType()).thenReturn(errorCode); + setupPlatformVpnWithSpecificExceptionAndItsErrorCode(exception, + VpnManager.CATEGORY_EVENT_IKE_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE, errorCode); + } + + @Test + public void testStartPlatformVpnFailedWithUnknownHostException() throws Exception { + final IkeNonProtocolException exception = mock(IkeNonProtocolException.class); + final UnknownHostException unknownHostException = new UnknownHostException(); + final int errorCode = VpnManager.ERROR_CODE_NETWORK_UNKNOWN_HOST; + when(exception.getCause()).thenReturn(unknownHostException); + setupPlatformVpnWithSpecificExceptionAndItsErrorCode(exception, + VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE, + errorCode); + } + + @Test + public void testStartPlatformVpnFailedWithIkeTimeoutException() throws Exception { + final IkeNonProtocolException exception = mock(IkeNonProtocolException.class); + final IkeTimeoutException ikeTimeoutException = + new IkeTimeoutException("IkeTimeoutException"); + final int errorCode = VpnManager.ERROR_CODE_NETWORK_PROTOCOL_TIMEOUT; + when(exception.getCause()).thenReturn(ikeTimeoutException); + setupPlatformVpnWithSpecificExceptionAndItsErrorCode(exception, + VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE, + errorCode); + } + + @Test + public void testStartPlatformVpnFailedWithIkeNetworkLostException() throws Exception { + final IkeNetworkLostException exception = new IkeNetworkLostException( + new Network(100)); + setupPlatformVpnWithSpecificExceptionAndItsErrorCode(exception, + VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE, + VpnManager.ERROR_CODE_NETWORK_LOST); + } + + @Test + public void testStartPlatformVpnFailedWithIOException() throws Exception { + final IkeNonProtocolException exception = mock(IkeNonProtocolException.class); + final IOException ioException = new IOException(); + final int errorCode = VpnManager.ERROR_CODE_NETWORK_IO; + when(exception.getCause()).thenReturn(ioException); + setupPlatformVpnWithSpecificExceptionAndItsErrorCode(exception, + VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE, + errorCode); } @Test