From b570e8654dfc95bd1fafa5b225c115047f881c89 Mon Sep 17 00:00:00 2001 From: Benedict Wong Date: Fri, 17 Jan 2020 19:33:55 -0800 Subject: [PATCH] Add always-on VPN support for platform VPNs This commit allows Platform VPNs to be started as part of always-on mode. Test: FrameworksNetTests passing, new tests added in subsequent CL Test: Manually tested. Change-Id: I5eda88e5b406a0e425eb7424665cf702e0979324 --- core/java/android/net/VpnManager.java | 6 +- .../android/server/ConnectivityService.java | 14 +- .../com/android/server/connectivity/Vpn.java | 150 ++++++++++++------ .../server/ConnectivityServiceTest.java | 3 +- .../android/server/connectivity/VpnTest.java | 86 +++++++--- 5 files changed, 182 insertions(+), 77 deletions(-) diff --git a/core/java/android/net/VpnManager.java b/core/java/android/net/VpnManager.java index f19ba0f5ef51a..2041cfb22ea81 100644 --- a/core/java/android/net/VpnManager.java +++ b/core/java/android/net/VpnManager.java @@ -126,7 +126,11 @@ public class VpnManager { return getIntentForConfirmation(); } - /** Delete the VPN profile configuration that was provisioned by the calling app */ + /** + * Delete the VPN profile configuration that was provisioned by the calling app + * + * @throws SecurityException if this would violate user settings + */ public void deleteProvisionedVpnProfile() { try { mService.deleteVpnProfile(mContext.getOpPackageName()); diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java index 0f36260d10168..7083281eaa7e9 100644 --- a/services/core/java/com/android/server/ConnectivityService.java +++ b/services/core/java/com/android/server/ConnectivityService.java @@ -4791,7 +4791,7 @@ public class ConnectivityService extends IConnectivityManager.Stub return false; } - return vpn.startAlwaysOnVpn(); + return vpn.startAlwaysOnVpn(mKeyStore); } } @@ -4806,7 +4806,7 @@ public class ConnectivityService extends IConnectivityManager.Stub Slog.w(TAG, "User " + userId + " has no Vpn configuration"); return false; } - return vpn.isAlwaysOnPackageSupported(packageName); + return vpn.isAlwaysOnPackageSupported(packageName, mKeyStore); } } @@ -4827,11 +4827,11 @@ public class ConnectivityService extends IConnectivityManager.Stub Slog.w(TAG, "User " + userId + " has no Vpn configuration"); return false; } - if (!vpn.setAlwaysOnPackage(packageName, lockdown, lockdownWhitelist)) { + if (!vpn.setAlwaysOnPackage(packageName, lockdown, lockdownWhitelist, mKeyStore)) { return false; } if (!startAlwaysOnVpn(userId)) { - vpn.setAlwaysOnPackage(null, false, null); + vpn.setAlwaysOnPackage(null, false, null, mKeyStore); return false; } } @@ -5017,7 +5017,7 @@ public class ConnectivityService extends IConnectivityManager.Stub loge("Starting user already has a VPN"); return; } - userVpn = new Vpn(mHandler.getLooper(), mContext, mNMS, userId); + userVpn = new Vpn(mHandler.getLooper(), mContext, mNMS, userId, mKeyStore); mVpns.put(userId, userVpn); if (mUserManager.getUserInfo(userId).isPrimary() && LockdownVpnTracker.isEnabled()) { updateLockdownVpn(); @@ -5088,7 +5088,7 @@ public class ConnectivityService extends IConnectivityManager.Stub if (TextUtils.equals(vpn.getAlwaysOnPackage(), packageName)) { Slog.d(TAG, "Restarting always-on VPN package " + packageName + " for user " + userId); - vpn.startAlwaysOnVpn(); + vpn.startAlwaysOnVpn(mKeyStore); } } } @@ -5110,7 +5110,7 @@ public class ConnectivityService extends IConnectivityManager.Stub if (TextUtils.equals(vpn.getAlwaysOnPackage(), packageName) && !isReplacing) { Slog.d(TAG, "Removing always-on VPN package " + packageName + " for user " + userId); - vpn.setAlwaysOnPackage(null, false, null); + vpn.setAlwaysOnPackage(null, false, null, mKeyStore); } } } diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 77f4093f0a18e..3138639c4a2b2 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -216,14 +216,14 @@ public class Vpn { * Whether to keep the connection active after rebooting, or upgrading or reinstalling. This * only applies to {@link VpnService} connections. */ - private boolean mAlwaysOn = false; + @VisibleForTesting protected boolean mAlwaysOn = false; /** * Whether to disable traffic outside of this VPN even when the VPN is not connected. System * apps can still bypass by choosing explicit networks. Has no effect if {@link mAlwaysOn} is - * not set. + * not set. Applies to all types of VPNs. */ - private boolean mLockdown = false; + @VisibleForTesting protected boolean mLockdown = false; /** * Set of packages in addition to the VPN app itself that can access the network directly when @@ -252,14 +252,14 @@ public class Vpn { private final int mUserHandle; public Vpn(Looper looper, Context context, INetworkManagementService netService, - @UserIdInt int userHandle) { - this(looper, context, netService, userHandle, + @UserIdInt int userHandle, @NonNull KeyStore keyStore) { + this(looper, context, netService, userHandle, keyStore, new SystemServices(context), new Ikev2SessionCreator()); } @VisibleForTesting protected Vpn(Looper looper, Context context, INetworkManagementService netService, - int userHandle, SystemServices systemServices, + int userHandle, @NonNull KeyStore keyStore, SystemServices systemServices, Ikev2SessionCreator ikev2SessionCreator) { mContext = context; mNetd = netService; @@ -285,7 +285,7 @@ public class Vpn { mNetworkCapabilities.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN); updateCapabilities(null /* defaultNetwork */); - loadAlwaysOnPackage(); + loadAlwaysOnPackage(keyStore); } /** @@ -437,23 +437,36 @@ public class Vpn { /** * Checks if a VPN app supports always-on mode. * - * In order to support the always-on feature, an app has to + *

In order to support the always-on feature, an app has to either have an installed + * PlatformVpnProfile, or: + * *

* * @param packageName the canonical package name of the VPN app + * @param keyStore the keystore instance to use for checking if the app has a Platform VPN + * profile installed. * @return {@code true} if and only if the VPN app exists and supports always-on mode */ - public boolean isAlwaysOnPackageSupported(String packageName) { + public boolean isAlwaysOnPackageSupported(String packageName, @NonNull KeyStore keyStore) { enforceSettingsPermission(); if (packageName == null) { return false; } + final long oldId = Binder.clearCallingIdentity(); + try { + if (getVpnProfilePrivileged(packageName, keyStore) != null) { + return true; + } + } finally { + Binder.restoreCallingIdentity(oldId); + } + PackageManager pm = mContext.getPackageManager(); ApplicationInfo appInfo = null; try { @@ -485,27 +498,31 @@ public class Vpn { } /** - * Configures an always-on VPN connection through a specific application. - * This connection is automatically granted and persisted after a reboot. + * Configures an always-on VPN connection through a specific application. This connection is + * automatically granted and persisted after a reboot. * - *

The designated package should exist and declare a {@link VpnService} in its - * manifest guarded by {@link android.Manifest.permission.BIND_VPN_SERVICE}, - * otherwise the call will fail. + *

The designated package should either have a PlatformVpnProfile installed, or declare a + * {@link VpnService} in its manifest guarded by {@link + * android.Manifest.permission.BIND_VPN_SERVICE}, otherwise the call will fail. * *

Note that this method does not check if the VPN app supports always-on mode. The check is - * delayed to {@link #startAlwaysOnVpn()}, which is always called immediately after this - * method in {@link android.net.IConnectivityManager#setAlwaysOnVpnPackage}. + * delayed to {@link #startAlwaysOnVpn()}, which is always called immediately after this method + * in {@link android.net.IConnectivityManager#setAlwaysOnVpnPackage}. * * @param packageName the package to designate as always-on VPN supplier. * @param lockdown whether to prevent traffic outside of a VPN, for example while connecting. * @param lockdownWhitelist packages to be whitelisted from lockdown. + * @param keyStore the Keystore instance to use for checking of PlatformVpnProfile(s) * @return {@code true} if the package has been set as always-on, {@code false} otherwise. */ public synchronized boolean setAlwaysOnPackage( - String packageName, boolean lockdown, List lockdownWhitelist) { + @Nullable String packageName, + boolean lockdown, + @Nullable List lockdownWhitelist, + @NonNull KeyStore keyStore) { enforceControlPermissionOrInternalCaller(); - if (setAlwaysOnPackageInternal(packageName, lockdown, lockdownWhitelist)) { + if (setAlwaysOnPackageInternal(packageName, lockdown, lockdownWhitelist, keyStore)) { saveAlwaysOnPackage(); return true; } @@ -513,20 +530,22 @@ public class Vpn { } /** - * Configures an always-on VPN connection through a specific application, the same as - * {@link #setAlwaysOnPackage}. + * Configures an always-on VPN connection through a specific application, the same as {@link + * #setAlwaysOnPackage}. * - * Does not perform permission checks. Does not persist any of the changes to storage. + *

Does not perform permission checks. Does not persist any of the changes to storage. * * @param packageName the package to designate as always-on VPN supplier. * @param lockdown whether to prevent traffic outside of a VPN, for example while connecting. * @param lockdownWhitelist packages to be whitelisted from lockdown. This is only used if - * {@code lockdown} is {@code true}. Packages must not contain commas. + * {@code lockdown} is {@code true}. Packages must not contain commas. + * @param keyStore the system keystore instance to check for profiles * @return {@code true} if the package has been set as always-on, {@code false} otherwise. */ @GuardedBy("this") private boolean setAlwaysOnPackageInternal( - String packageName, boolean lockdown, List lockdownWhitelist) { + @Nullable String packageName, boolean lockdown, + @Nullable List lockdownWhitelist, @NonNull KeyStore keyStore) { if (VpnConfig.LEGACY_VPN.equals(packageName)) { Log.w(TAG, "Not setting legacy VPN \"" + packageName + "\" as always-on."); return false; @@ -542,11 +561,18 @@ public class Vpn { } if (packageName != null) { - // TODO: Give the minimum permission possible; if there is a Platform VPN profile, only - // grant ACTIVATE_PLATFORM_VPN. - // Pre-authorize new always-on VPN package. Grant the full ACTIVATE_VPN appop, allowing - // both VpnService and Platform VPNs. - if (!setPackageAuthorization(packageName, VpnManager.TYPE_VPN_SERVICE)) { + final VpnProfile profile; + final long oldId = Binder.clearCallingIdentity(); + try { + profile = getVpnProfilePrivileged(packageName, keyStore); + } finally { + Binder.restoreCallingIdentity(oldId); + } + + // Pre-authorize new always-on VPN package. + final int grantType = + (profile == null) ? VpnManager.TYPE_VPN_SERVICE : VpnManager.TYPE_VPN_PLATFORM; + if (!setPackageAuthorization(packageName, grantType)) { return false; } mAlwaysOn = true; @@ -611,11 +637,9 @@ public class Vpn { } } - /** - * Load the always-on package and lockdown config from Settings.Secure - */ + /** Load the always-on package and lockdown config from Settings. */ @GuardedBy("this") - private void loadAlwaysOnPackage() { + private void loadAlwaysOnPackage(@NonNull KeyStore keyStore) { final long token = Binder.clearCallingIdentity(); try { final String alwaysOnPackage = mSystemServices.settingsSecureGetStringForUser( @@ -626,17 +650,21 @@ public class Vpn { Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN_WHITELIST, mUserHandle); final List whitelistedPackages = TextUtils.isEmpty(whitelistString) ? Collections.emptyList() : Arrays.asList(whitelistString.split(",")); - setAlwaysOnPackageInternal(alwaysOnPackage, alwaysOnLockdown, whitelistedPackages); + setAlwaysOnPackageInternal( + alwaysOnPackage, alwaysOnLockdown, whitelistedPackages, keyStore); } finally { Binder.restoreCallingIdentity(token); } } /** + * Starts the currently selected always-on VPN + * + * @param keyStore the keyStore instance for looking up PlatformVpnProfile(s) * @return {@code true} if the service was started, the service was already connected, or there - * was no always-on VPN to start. {@code false} otherwise. + * was no always-on VPN to start. {@code false} otherwise. */ - public boolean startAlwaysOnVpn() { + public boolean startAlwaysOnVpn(@NonNull KeyStore keyStore) { final String alwaysOnPackage; synchronized (this) { alwaysOnPackage = getAlwaysOnPackage(); @@ -645,8 +673,8 @@ public class Vpn { return true; } // Remove always-on VPN if it's not supported. - if (!isAlwaysOnPackageSupported(alwaysOnPackage)) { - setAlwaysOnPackage(null, false, null); + if (!isAlwaysOnPackageSupported(alwaysOnPackage, keyStore)) { + setAlwaysOnPackage(null, false, null, keyStore); return false; } // Skip if the service is already established. This isn't bulletproof: it's not bound @@ -657,10 +685,24 @@ public class Vpn { } } - // Tell the OS that background services in this app need to be allowed for - // a short time, so we can bootstrap the VPN service. final long oldId = Binder.clearCallingIdentity(); try { + // Prefer VPN profiles, if any exist. + VpnProfile profile = getVpnProfilePrivileged(alwaysOnPackage, keyStore); + if (profile != null) { + startVpnProfilePrivileged(profile, alwaysOnPackage); + + // If the above startVpnProfilePrivileged() call returns, the Ikev2VpnProfile was + // correctly parsed, and the VPN has started running in a different thread. The only + // other possibility is that the above call threw an exception, which will be + // caught below, and returns false (clearing the always-on VPN). Once started, the + // Platform VPN cannot permanantly fail, and is resiliant to temporary failures. It + // will continue retrying until shut down by the user, or always-on is toggled off. + return true; + } + + // Tell the OS that background services in this app need to be allowed for + // a short time, so we can bootstrap the VPN service. DeviceIdleInternal idleController = LocalServices.getService(DeviceIdleInternal.class); idleController.addPowerSaveTempWhitelistApp(Process.myUid(), alwaysOnPackage, @@ -675,6 +717,9 @@ public class Vpn { Log.e(TAG, "VpnService " + serviceIntent + " failed to start", e); return false; } + } catch (Exception e) { + Log.e(TAG, "Error starting always-on VPN", e); + return false; } finally { Binder.restoreCallingIdentity(oldId); } @@ -2820,6 +2865,10 @@ public class Vpn { return isVpnProfilePreConsented(mContext, packageName); } + private boolean isCurrentIkev2VpnLocked(@NonNull String packageName) { + return isCurrentPreparedPackage(packageName) && mVpnRunner instanceof IkeV2VpnRunner; + } + /** * Deletes an app-provisioned VPN profile. * @@ -2836,6 +2885,17 @@ public class Vpn { Binder.withCleanCallingIdentity( () -> { + // If this profile is providing the current VPN, turn it off, disabling + // always-on as well if enabled. + if (isCurrentIkev2VpnLocked(packageName)) { + if (mAlwaysOn) { + // Will transitively call prepareInternal(VpnConfig.LEGACY_VPN). + setAlwaysOnPackage(null, false, null, keyStore); + } else { + prepareInternal(VpnConfig.LEGACY_VPN); + } + } + keyStore.delete(getProfileNameForPackage(packageName), Process.SYSTEM_UID); }); } @@ -2946,11 +3006,9 @@ public class Vpn { // To stop the VPN profile, the caller must be the current prepared package and must be // running an Ikev2VpnProfile. - if (!isCurrentPreparedPackage(packageName) && mVpnRunner instanceof IkeV2VpnRunner) { - return; + if (isCurrentIkev2VpnLocked(packageName)) { + prepareInternal(VpnConfig.LEGACY_VPN); } - - prepareInternal(VpnConfig.LEGACY_VPN); } /** diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java index 957683e05de7e..08869751b93e7 100644 --- a/tests/net/java/com/android/server/ConnectivityServiceTest.java +++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java @@ -204,6 +204,7 @@ import android.os.SystemClock; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; +import android.security.KeyStore; import android.system.Os; import android.test.mock.MockContentResolver; import android.text.TextUtils; @@ -1019,7 +1020,7 @@ public class ConnectivityServiceTest { public MockVpn(int userId) { super(startHandlerThreadAndReturnLooper(), mServiceContext, mNetworkManagementService, - userId); + userId, mock(KeyStore.class)); } public void setNetworkAgent(TestNetworkAgentWrapper agent) { diff --git a/tests/net/java/com/android/server/connectivity/VpnTest.java b/tests/net/java/com/android/server/connectivity/VpnTest.java index ac1c51837e933..0e3b797613192 100644 --- a/tests/net/java/com/android/server/connectivity/VpnTest.java +++ b/tests/net/java/com/android/server/connectivity/VpnTest.java @@ -72,6 +72,7 @@ import android.os.Looper; import android.os.Process; import android.os.UserHandle; import android.os.UserManager; +import android.provider.Settings; import android.security.Credentials; import android.security.KeyStore; import android.util.ArrayMap; @@ -260,17 +261,17 @@ public class VpnTest { assertFalse(vpn.getLockdown()); // Set always-on without lockdown. - assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false, Collections.emptyList())); + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false, Collections.emptyList(), mKeyStore)); assertTrue(vpn.getAlwaysOn()); assertFalse(vpn.getLockdown()); // Set always-on with lockdown. - assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, Collections.emptyList())); + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, Collections.emptyList(), mKeyStore)); assertTrue(vpn.getAlwaysOn()); assertTrue(vpn.getLockdown()); // Remove always-on configuration. - assertTrue(vpn.setAlwaysOnPackage(null, false, Collections.emptyList())); + assertTrue(vpn.setAlwaysOnPackage(null, false, Collections.emptyList(), mKeyStore)); assertFalse(vpn.getAlwaysOn()); assertFalse(vpn.getLockdown()); } @@ -284,11 +285,11 @@ public class VpnTest { assertUnblocked(vpn, user.start + PKG_UIDS[0], user.start + PKG_UIDS[1], user.start + PKG_UIDS[2], user.start + PKG_UIDS[3]); // Set always-on without lockdown. - assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false, null)); + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false, null, mKeyStore)); assertUnblocked(vpn, user.start + PKG_UIDS[0], user.start + PKG_UIDS[1], user.start + PKG_UIDS[2], user.start + PKG_UIDS[3]); // Set always-on with lockdown. - assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, null)); + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, null, mKeyStore)); verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(new UidRange[] { new UidRange(user.start, user.start + PKG_UIDS[1] - 1), new UidRange(user.start + PKG_UIDS[1] + 1, user.stop) @@ -297,7 +298,7 @@ public class VpnTest { assertUnblocked(vpn, user.start + PKG_UIDS[1]); // Switch to another app. - assertTrue(vpn.setAlwaysOnPackage(PKGS[3], true, null)); + assertTrue(vpn.setAlwaysOnPackage(PKGS[3], true, null, mKeyStore)); verify(mNetService).setAllowOnlyVpnForUids(eq(false), aryEq(new UidRange[] { new UidRange(user.start, user.start + PKG_UIDS[1] - 1), new UidRange(user.start + PKG_UIDS[1] + 1, user.stop) @@ -316,7 +317,8 @@ public class VpnTest { final UidRange user = UidRange.createForUser(primaryUser.id); // Set always-on with lockdown and whitelist app PKGS[2] from lockdown. - assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, Collections.singletonList(PKGS[2]))); + assertTrue(vpn.setAlwaysOnPackage( + PKGS[1], true, Collections.singletonList(PKGS[2]), mKeyStore)); verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(new UidRange[] { new UidRange(user.start, user.start + PKG_UIDS[1] - 1), new UidRange(user.start + PKG_UIDS[2] + 1, user.stop) @@ -325,7 +327,8 @@ public class VpnTest { assertUnblocked(vpn, user.start + PKG_UIDS[1], user.start + PKG_UIDS[2]); // Change whitelisted app to PKGS[3]. - assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, Collections.singletonList(PKGS[3]))); + assertTrue(vpn.setAlwaysOnPackage( + PKGS[1], true, Collections.singletonList(PKGS[3]), mKeyStore)); verify(mNetService).setAllowOnlyVpnForUids(eq(false), aryEq(new UidRange[] { new UidRange(user.start + PKG_UIDS[2] + 1, user.stop) })); @@ -337,7 +340,8 @@ public class VpnTest { assertUnblocked(vpn, user.start + PKG_UIDS[1], user.start + PKG_UIDS[3]); // Change the VPN app. - assertTrue(vpn.setAlwaysOnPackage(PKGS[0], true, Collections.singletonList(PKGS[3]))); + assertTrue(vpn.setAlwaysOnPackage( + PKGS[0], true, Collections.singletonList(PKGS[3]), mKeyStore)); verify(mNetService).setAllowOnlyVpnForUids(eq(false), aryEq(new UidRange[] { new UidRange(user.start, user.start + PKG_UIDS[1] - 1), new UidRange(user.start + PKG_UIDS[1] + 1, user.start + PKG_UIDS[3] - 1) @@ -350,7 +354,7 @@ public class VpnTest { assertUnblocked(vpn, user.start + PKG_UIDS[0], user.start + PKG_UIDS[3]); // Remove the whitelist. - assertTrue(vpn.setAlwaysOnPackage(PKGS[0], true, null)); + assertTrue(vpn.setAlwaysOnPackage(PKGS[0], true, null, mKeyStore)); verify(mNetService).setAllowOnlyVpnForUids(eq(false), aryEq(new UidRange[] { new UidRange(user.start + PKG_UIDS[0] + 1, user.start + PKG_UIDS[3] - 1), new UidRange(user.start + PKG_UIDS[3] + 1, user.stop) @@ -363,7 +367,8 @@ public class VpnTest { assertUnblocked(vpn, user.start + PKG_UIDS[0]); // Add the whitelist. - assertTrue(vpn.setAlwaysOnPackage(PKGS[0], true, Collections.singletonList(PKGS[1]))); + assertTrue(vpn.setAlwaysOnPackage( + PKGS[0], true, Collections.singletonList(PKGS[1]), mKeyStore)); verify(mNetService).setAllowOnlyVpnForUids(eq(false), aryEq(new UidRange[] { new UidRange(user.start + PKG_UIDS[0] + 1, user.stop) })); @@ -375,12 +380,13 @@ public class VpnTest { assertUnblocked(vpn, user.start + PKG_UIDS[0], user.start + PKG_UIDS[1]); // Try whitelisting a package with a comma, should be rejected. - assertFalse(vpn.setAlwaysOnPackage(PKGS[0], true, Collections.singletonList("a.b,c.d"))); + assertFalse(vpn.setAlwaysOnPackage( + PKGS[0], true, Collections.singletonList("a.b,c.d"), mKeyStore)); // Pass a non-existent packages in the whitelist, they (and only they) should be ignored. // Whitelisted package should change from PGKS[1] to PKGS[2]. - assertTrue(vpn.setAlwaysOnPackage(PKGS[0], true, - Arrays.asList("com.foo.app", PKGS[2], "com.bar.app"))); + assertTrue(vpn.setAlwaysOnPackage( + PKGS[0], true, Arrays.asList("com.foo.app", PKGS[2], "com.bar.app"), mKeyStore)); verify(mNetService).setAllowOnlyVpnForUids(eq(false), aryEq(new UidRange[]{ new UidRange(user.start + PKG_UIDS[0] + 1, user.start + PKG_UIDS[1] - 1), new UidRange(user.start + PKG_UIDS[1] + 1, user.stop) @@ -405,7 +411,7 @@ public class VpnTest { final UidRange profile = UidRange.createForUser(tempProfile.id); // Set lockdown. - assertTrue(vpn.setAlwaysOnPackage(PKGS[3], true, null)); + assertTrue(vpn.setAlwaysOnPackage(PKGS[3], true, null, mKeyStore)); verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(new UidRange[] { new UidRange(user.start, user.start + PKG_UIDS[3] - 1), new UidRange(user.start + PKG_UIDS[3] + 1, user.stop) @@ -499,22 +505,22 @@ public class VpnTest { .thenReturn(Collections.singletonList(resInfo)); // null package name should return false - assertFalse(vpn.isAlwaysOnPackageSupported(null)); + assertFalse(vpn.isAlwaysOnPackageSupported(null, mKeyStore)); // Pre-N apps are not supported appInfo.targetSdkVersion = VERSION_CODES.M; - assertFalse(vpn.isAlwaysOnPackageSupported(PKGS[0])); + assertFalse(vpn.isAlwaysOnPackageSupported(PKGS[0], mKeyStore)); // N+ apps are supported by default appInfo.targetSdkVersion = VERSION_CODES.N; - assertTrue(vpn.isAlwaysOnPackageSupported(PKGS[0])); + assertTrue(vpn.isAlwaysOnPackageSupported(PKGS[0], mKeyStore)); // Apps that opt out explicitly are not supported appInfo.targetSdkVersion = VERSION_CODES.CUR_DEVELOPMENT; Bundle metaData = new Bundle(); metaData.putBoolean(VpnService.SERVICE_META_DATA_SUPPORTS_ALWAYS_ON, false); svcInfo.metaData = metaData; - assertFalse(vpn.isAlwaysOnPackageSupported(PKGS[0])); + assertFalse(vpn.isAlwaysOnPackageSupported(PKGS[0], mKeyStore)); } @Test @@ -531,7 +537,7 @@ public class VpnTest { .cancelAsUser(anyString(), anyInt(), eq(userHandle)); // Start showing a notification for disconnected once always-on. - vpn.setAlwaysOnPackage(PKGS[0], false, null); + vpn.setAlwaysOnPackage(PKGS[0], false, null, mKeyStore); order.verify(mNotificationManager) .notifyAsUser(anyString(), anyInt(), any(), eq(userHandle)); @@ -545,7 +551,7 @@ public class VpnTest { .notifyAsUser(anyString(), anyInt(), any(), eq(userHandle)); // Notification should be cleared after unsetting always-on package. - vpn.setAlwaysOnPackage(null, false, null); + vpn.setAlwaysOnPackage(null, false, null, mKeyStore); order.verify(mNotificationManager).cancelAsUser(anyString(), anyInt(), eq(userHandle)); } @@ -920,12 +926,48 @@ public class VpnTest { eq(AppOpsManager.MODE_IGNORED)); } + private void setAndVerifyAlwaysOnPackage(Vpn vpn, int uid, boolean lockdownEnabled) { + assertTrue(vpn.setAlwaysOnPackage(TEST_VPN_PKG, lockdownEnabled, null, mKeyStore)); + + verify(mKeyStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG))); + verify(mAppOps).setMode( + eq(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN), eq(uid), eq(TEST_VPN_PKG), + eq(AppOpsManager.MODE_ALLOWED)); + + verify(mSystemServices).settingsSecurePutStringForUser( + eq(Settings.Secure.ALWAYS_ON_VPN_APP), eq(TEST_VPN_PKG), eq(primaryUser.id)); + verify(mSystemServices).settingsSecurePutIntForUser( + eq(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN), eq(lockdownEnabled ? 1 : 0), + eq(primaryUser.id)); + verify(mSystemServices).settingsSecurePutStringForUser( + eq(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN_WHITELIST), eq(""), eq(primaryUser.id)); + } + + @Test + public void testSetAndStartAlwaysOnVpn() throws Exception { + final Vpn vpn = createVpn(primaryUser.id); + setMockedUsers(primaryUser); + + // UID checks must return a different UID; otherwise it'll be treated as already prepared. + final int uid = Process.myUid() + 1; + when(mPackageManager.getPackageUidAsUser(eq(TEST_VPN_PKG), anyInt())) + .thenReturn(uid); + when(mKeyStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) + .thenReturn(mVpnProfile.encode()); + + setAndVerifyAlwaysOnPackage(vpn, uid, false); + assertTrue(vpn.startAlwaysOnVpn(mKeyStore)); + + // TODO: Test the Ikev2VpnRunner started up properly. Relies on utility methods added in + // a subsequent CL. + } + /** * Mock some methods of vpn object. */ private Vpn createVpn(@UserIdInt int userId) { return new Vpn(Looper.myLooper(), mContext, mNetService, - userId, mSystemServices, mIkev2SessionCreator); + userId, mKeyStore, mSystemServices, mIkev2SessionCreator); } private static void assertBlocked(Vpn vpn, int... uids) {