diff --git a/api/current.txt b/api/current.txt index 0912662b2da57..8b39c2079e25a 100755 --- a/api/current.txt +++ b/api/current.txt @@ -6450,6 +6450,7 @@ package android.app.admin { method @Nullable public String[] getAccountTypesWithManagementDisabled(); method @Nullable public java.util.List getActiveAdmins(); method @NonNull public java.util.Set getAffiliationIds(@NonNull android.content.ComponentName); + method public java.util.List getAlwaysOnVpnLockdownWhitelist(@NonNull android.content.ComponentName); method @Nullable public String getAlwaysOnVpnPackage(@NonNull android.content.ComponentName); method @WorkerThread @NonNull public android.os.Bundle getApplicationRestrictions(@Nullable android.content.ComponentName, String); method @Deprecated @Nullable public String getApplicationRestrictionsManagingPackage(@NonNull android.content.ComponentName); @@ -6519,6 +6520,7 @@ package android.app.admin { method public boolean isActivePasswordSufficient(); method public boolean isAdminActive(@NonNull android.content.ComponentName); method public boolean isAffiliatedUser(); + method public boolean isAlwaysOnVpnLockdownEnabled(@NonNull android.content.ComponentName); method public boolean isApplicationHidden(@NonNull android.content.ComponentName, String); method public boolean isBackupServiceEnabled(@NonNull android.content.ComponentName); method @Deprecated public boolean isCallerApplicationRestrictionsManagingPackage(); @@ -6556,6 +6558,7 @@ package android.app.admin { method public void setAccountManagementDisabled(@NonNull android.content.ComponentName, String, boolean); method public void setAffiliationIds(@NonNull android.content.ComponentName, @NonNull java.util.Set); method public void setAlwaysOnVpnPackage(@NonNull android.content.ComponentName, @Nullable String, boolean) throws android.content.pm.PackageManager.NameNotFoundException, java.lang.UnsupportedOperationException; + method public void setAlwaysOnVpnPackage(@NonNull android.content.ComponentName, @Nullable String, boolean, @Nullable java.util.List) throws android.content.pm.PackageManager.NameNotFoundException, java.lang.UnsupportedOperationException; method public boolean setApplicationHidden(@NonNull android.content.ComponentName, String, boolean); method @WorkerThread public void setApplicationRestrictions(@Nullable android.content.ComponentName, String, android.os.Bundle); method @Deprecated public void setApplicationRestrictionsManagingPackage(@NonNull android.content.ComponentName, @Nullable String) throws android.content.pm.PackageManager.NameNotFoundException; diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index a965e5f4d990c..1507b3e26f198 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -4468,12 +4468,17 @@ public class DevicePolicyManager { return null; } + /** + * Service-specific error code used in implementation of {@code setAlwaysOnVpnPackage} methods. + * @hide + */ + public static final int ERROR_VPN_PACKAGE_NOT_FOUND = 1; + /** * Called by a device or profile owner to configure an always-on VPN connection through a * specific application for the current user. This connection is automatically granted and * persisted after a reboot. - *

- * To support the always-on feature, an app must + *

To support the always-on feature, an app must *

    *
  • declare a {@link android.net.VpnService} in its manifest, guarded by * {@link android.Manifest.permission#BIND_VPN_SERVICE};
  • @@ -4482,12 +4487,13 @@ public class DevicePolicyManager { * {@link android.net.VpnService#SERVICE_META_DATA_SUPPORTS_ALWAYS_ON}. *
* The call will fail if called with the package name of an unsupported VPN app. + *

Enabling lockdown via {@code lockdownEnabled} argument carries the risk that any failure + * of the VPN provider could break networking for all apps. * * @param vpnPackage The package name for an installed VPN app on the device, or {@code null} to * remove an existing always-on VPN configuration. * @param lockdownEnabled {@code true} to disallow networking when the VPN is not connected or - * {@code false} otherwise. This carries the risk that any failure of the VPN provider - * could break networking for all apps. This has no effect when clearing. + * {@code false} otherwise. This has no effect when clearing. * @throws SecurityException if {@code admin} is not a device or a profile owner. * @throws NameNotFoundException if {@code vpnPackage} is not installed. * @throws UnsupportedOperationException if {@code vpnPackage} exists but does not support being @@ -4496,11 +4502,46 @@ public class DevicePolicyManager { public void setAlwaysOnVpnPackage(@NonNull ComponentName admin, @Nullable String vpnPackage, boolean lockdownEnabled) throws NameNotFoundException, UnsupportedOperationException { + setAlwaysOnVpnPackage(admin, vpnPackage, lockdownEnabled, Collections.emptyList()); + } + + /** + * A version of {@link #setAlwaysOnVpnPackage(ComponentName, String, boolean)} that allows the + * admin to specify a set of apps that should be able to access the network directly when VPN + * is not connected. When VPN connects these apps switch over to VPN if allowed to use that VPN. + * System apps can always bypass VPN. + *

Note that the system doesn't update the whitelist when packages are installed or + * uninstalled, the admin app must call this method to keep the list up to date. + * + * @param vpnPackage package name for an installed VPN app on the device, or {@code null} + * to remove an existing always-on VPN configuration + * @param lockdownEnabled {@code true} to disallow networking when the VPN is not connected or + * {@code false} otherwise. This has no effect when clearing. + * @param lockdownWhitelist Packages that will be able to access the network directly when VPN + * is in lockdown mode but not connected. Has no effect when clearing. + * @throws SecurityException if {@code admin} is not a device or a profile + * owner. + * @throws NameNotFoundException if {@code vpnPackage} or one of + * {@code lockdownWhitelist} is not installed. + * @throws UnsupportedOperationException if {@code vpnPackage} exists but does + * not support being set as always-on, or if always-on VPN is not + * available. + */ + public void setAlwaysOnVpnPackage(@NonNull ComponentName admin, @Nullable String vpnPackage, + boolean lockdownEnabled, @Nullable List lockdownWhitelist) + throws NameNotFoundException, UnsupportedOperationException { throwIfParentInstance("setAlwaysOnVpnPackage"); if (mService != null) { try { - if (!mService.setAlwaysOnVpnPackage(admin, vpnPackage, lockdownEnabled)) { - throw new NameNotFoundException(vpnPackage); + mService.setAlwaysOnVpnPackage( + admin, vpnPackage, lockdownEnabled, lockdownWhitelist); + } catch (ServiceSpecificException e) { + switch (e.errorCode) { + case ERROR_VPN_PACKAGE_NOT_FOUND: + throw new NameNotFoundException(e.getMessage()); + default: + throw new RuntimeException( + "Unknown error setting always-on VPN: " + e.errorCode); } } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -4508,6 +4549,51 @@ public class DevicePolicyManager { } } + /** + * Called by device or profile owner to query whether current always-on VPN is configured in + * lockdown mode. Returns {@code false} when no always-on configuration is set. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * + * @throws SecurityException if {@code admin} is not a device or a profile owner. + * + * @see #setAlwaysOnVpnPackage(ComponentName, String, boolean) + */ + public boolean isAlwaysOnVpnLockdownEnabled(@NonNull ComponentName admin) { + throwIfParentInstance("isAlwaysOnVpnLockdownEnabled"); + if (mService != null) { + try { + return mService.isAlwaysOnVpnLockdownEnabled(admin); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return false; + } + + /** + * Called by device or profile owner to query the list of packages that are allowed to access + * the network directly when always-on VPN is in lockdown mode but not connected. Returns + * {@code null} when always-on VPN is not active or not in lockdown mode. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * + * @throws SecurityException if {@code admin} is not a device or a profile owner. + * + * @see #setAlwaysOnVpnPackage(ComponentName, String, boolean, List) + */ + public List getAlwaysOnVpnLockdownWhitelist(@NonNull ComponentName admin) { + throwIfParentInstance("getAlwaysOnVpnLockdownWhitelist"); + if (mService != null) { + try { + return mService.getAlwaysOnVpnLockdownWhitelist(admin); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return null; + } + /** * Called by a device or profile owner to read the name of the package administering an * always-on VPN connection for the current user. If there is no such package, or the always-on diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index 37508cdc1119f..00463028a685a 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -182,8 +182,10 @@ interface IDevicePolicyManager { void setCertInstallerPackage(in ComponentName who, String installerPackage); String getCertInstallerPackage(in ComponentName who); - boolean setAlwaysOnVpnPackage(in ComponentName who, String vpnPackage, boolean lockdown); + boolean setAlwaysOnVpnPackage(in ComponentName who, String vpnPackage, boolean lockdown, in List lockdownWhitelist); String getAlwaysOnVpnPackage(in ComponentName who); + boolean isAlwaysOnVpnLockdownEnabled(in ComponentName who); + List getAlwaysOnVpnLockdownWhitelist(in ComponentName who); void addPersistentPreferredActivity(in ComponentName admin, in IntentFilter filter, in ComponentName activity); void clearPackagePersistentPreferredActivities(in ComponentName admin, String packageName); diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index 5bb24bab6e48e..243b0ebab8f90 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -1014,14 +1014,20 @@ public class ConnectivityManager { * to remove an existing always-on VPN configuration. * @param lockdownEnabled {@code true} to disallow networking when the VPN is not connected or * {@code false} otherwise. + * @param lockdownWhitelist The list of packages that are allowed to access network directly + * when VPN is in lockdown mode but is not running. Non-existent packages are ignored so + * this method must be called when a package that should be whitelisted is installed or + * uninstalled. * @return {@code true} if the package is set as always-on VPN controller; * {@code false} otherwise. * @hide */ + @RequiresPermission(android.Manifest.permission.CONTROL_ALWAYS_ON_VPN) public boolean setAlwaysOnVpnPackageForUser(int userId, @Nullable String vpnPackage, - boolean lockdownEnabled) { + boolean lockdownEnabled, @Nullable List lockdownWhitelist) { try { - return mService.setAlwaysOnVpnPackage(userId, vpnPackage, lockdownEnabled); + return mService.setAlwaysOnVpnPackage( + userId, vpnPackage, lockdownEnabled, lockdownWhitelist); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1036,6 +1042,7 @@ public class ConnectivityManager { * or {@code null} if none is set. * @hide */ + @RequiresPermission(android.Manifest.permission.CONTROL_ALWAYS_ON_VPN) public String getAlwaysOnVpnPackageForUser(int userId) { try { return mService.getAlwaysOnVpnPackage(userId); @@ -1044,6 +1051,36 @@ public class ConnectivityManager { } } + /** + * @return whether always-on VPN is in lockdown mode. + * + * @hide + **/ + @RequiresPermission(android.Manifest.permission.CONTROL_ALWAYS_ON_VPN) + public boolean isVpnLockdownEnabled(int userId) { + try { + return mService.isVpnLockdownEnabled(userId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + + } + + /** + * @return the list of packages that are allowed to access network when always-on VPN is in + * lockdown mode but not connected. Returns {@code null} when VPN lockdown is not active. + * + * @hide + **/ + @RequiresPermission(android.Manifest.permission.CONTROL_ALWAYS_ON_VPN) + public List getVpnLockdownWhitelist(int userId) { + try { + return mService.getVpnLockdownWhitelist(userId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** * Returns details about the currently active default data network * for a given uid. This is for internal use only to avoid spying diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl index e97060a0a5999..fd7360fd4c174 100644 --- a/core/java/android/net/IConnectivityManager.aidl +++ b/core/java/android/net/IConnectivityManager.aidl @@ -125,8 +125,11 @@ interface IConnectivityManager boolean updateLockdownVpn(); boolean isAlwaysOnVpnPackageSupported(int userId, String packageName); - boolean setAlwaysOnVpnPackage(int userId, String packageName, boolean lockdown); + boolean setAlwaysOnVpnPackage(int userId, String packageName, boolean lockdown, + in List lockdownWhitelist); String getAlwaysOnVpnPackage(int userId); + boolean isVpnLockdownEnabled(int userId); + List getVpnLockdownWhitelist(int userId); int checkMobileProvisioning(int suggestedTimeOutMs); diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 3d9628566ab79..1bc5a8ab84c2e 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -5672,6 +5672,16 @@ public final class Settings { */ public static final String ALWAYS_ON_VPN_LOCKDOWN = "always_on_vpn_lockdown"; + /** + * Comma separated list of packages that are allowed to access the network when VPN is in + * lockdown mode but not running. + * @see #ALWAYS_ON_VPN_LOCKDOWN + * + * @hide + */ + public static final String ALWAYS_ON_VPN_LOCKDOWN_WHITELIST = + "always_on_vpn_lockdown_whitelist"; + /** * Whether applications can be installed for this user via the system's * {@link Intent#ACTION_INSTALL_PACKAGE} mechanism. diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 0fdd14143a2f0..186cc8c028cec 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -3231,6 +3231,12 @@ android:protectionLevel="signature|privileged" /> + + + lockdownWhitelist) { + enforceControlAlwaysOnVpnPermission(); enforceCrossUserPermission(userId); synchronized (mVpns) { @@ -4131,11 +4144,11 @@ public class ConnectivityService extends IConnectivityManager.Stub Slog.w(TAG, "User " + userId + " has no Vpn configuration"); return false; } - if (!vpn.setAlwaysOnPackage(packageName, lockdown)) { + if (!vpn.setAlwaysOnPackage(packageName, lockdown, lockdownWhitelist)) { return false; } if (!startAlwaysOnVpn(userId)) { - vpn.setAlwaysOnPackage(null, false); + vpn.setAlwaysOnPackage(null, false, null); return false; } } @@ -4144,7 +4157,7 @@ public class ConnectivityService extends IConnectivityManager.Stub @Override public String getAlwaysOnVpnPackage(int userId) { - enforceConnectivityInternalPermission(); + enforceControlAlwaysOnVpnPermission(); enforceCrossUserPermission(userId); synchronized (mVpns) { @@ -4157,6 +4170,36 @@ public class ConnectivityService extends IConnectivityManager.Stub } } + @Override + public boolean isVpnLockdownEnabled(int userId) { + enforceControlAlwaysOnVpnPermission(); + enforceCrossUserPermission(userId); + + synchronized (mVpns) { + Vpn vpn = mVpns.get(userId); + if (vpn == null) { + Slog.w(TAG, "User " + userId + " has no Vpn configuration"); + return false; + } + return vpn.getLockdown(); + } + } + + @Override + public List getVpnLockdownWhitelist(int userId) { + enforceControlAlwaysOnVpnPermission(); + enforceCrossUserPermission(userId); + + synchronized (mVpns) { + Vpn vpn = mVpns.get(userId); + if (vpn == null) { + Slog.w(TAG, "User " + userId + " has no Vpn configuration"); + return null; + } + return vpn.getLockdownWhitelist(); + } + } + @Override public int checkMobileProvisioning(int suggestedTimeOutMs) { // TODO: Remove? Any reason to trigger a provisioning check? @@ -4386,7 +4429,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); + vpn.setAlwaysOnPackage(null, false, null); } } } @@ -6266,7 +6309,7 @@ public class ConnectivityService extends IConnectivityManager.Stub synchronized (mVpns) { final String alwaysOnPackage = getAlwaysOnVpnPackage(userId); if (alwaysOnPackage != null) { - setAlwaysOnVpnPackage(userId, null, false); + setAlwaysOnVpnPackage(userId, null, false, null); setVpnPackageAuthorization(alwaysOnPackage, userId, false); } diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index c72c9ddf3f7a6..c520359e4b370 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -151,7 +151,7 @@ public class Vpn { .divide(BigInteger.valueOf(100)); } // How many routes to evaluate before bailing and declaring this Vpn should provide - // the INTERNET capability. This is necessary because computing the adress space is + // the INTERNET capability. This is necessary because computing the address space is // O(n²) and this is running in the system service, so a limit is needed to alleviate // the risk of attack. // This is taken as a total of IPv4 + IPV6 routes for simplicity, but the algorithm @@ -193,6 +193,12 @@ public class Vpn { */ private boolean mLockdown = false; + /** + * Set of packages in addition to the VPN app itself that can access the network directly when + * VPN is not connected even if {@code mLockdown} is set. + */ + private @NonNull List mLockdownWhitelist = Collections.emptyList(); + /** * List of UIDs for which networking should be blocked until VPN is ready, during brief periods * when VPN is not running. For example, during system startup or after a crash. @@ -320,9 +326,9 @@ public class Vpn { * * Used to enable/disable legacy VPN lockdown. * - * This uses the same ip rule mechanism as {@link #setAlwaysOnPackage(String, boolean)}; - * previous settings from calling that function will be replaced and saved with the - * always-on state. + * This uses the same ip rule mechanism as + * {@link #setAlwaysOnPackage(String, boolean, List)}; previous settings from calling + * that function will be replaced and saved with the always-on state. * * @param lockdown whether to prevent all traffic outside of a VPN. */ @@ -419,12 +425,14 @@ public class Vpn { * * @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. * @return {@code true} if the package has been set as always-on, {@code false} otherwise. */ - public synchronized boolean setAlwaysOnPackage(String packageName, boolean lockdown) { + public synchronized boolean setAlwaysOnPackage( + String packageName, boolean lockdown, List lockdownWhitelist) { enforceControlPermissionOrInternalCaller(); - if (setAlwaysOnPackageInternal(packageName, lockdown)) { + if (setAlwaysOnPackageInternal(packageName, lockdown, lockdownWhitelist)) { saveAlwaysOnPackage(); return true; } @@ -439,15 +447,27 @@ public class Vpn { * * @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. * @return {@code true} if the package has been set as always-on, {@code false} otherwise. */ @GuardedBy("this") - private boolean setAlwaysOnPackageInternal(String packageName, boolean lockdown) { + private boolean setAlwaysOnPackageInternal( + String packageName, boolean lockdown, List lockdownWhitelist) { if (VpnConfig.LEGACY_VPN.equals(packageName)) { Log.w(TAG, "Not setting legacy VPN \"" + packageName + "\" as always-on."); return false; } + if (lockdownWhitelist != null) { + for (String pkg : lockdownWhitelist) { + if (pkg.contains(",")) { + Log.w(TAG, "Not setting always-on vpn, invalid whitelisted package: " + pkg); + return false; + } + } + } + if (packageName != null) { // Pre-authorize new always-on VPN package. if (!setPackageAuthorization(packageName, true)) { @@ -460,13 +480,18 @@ public class Vpn { } mLockdown = (mAlwaysOn && lockdown); + mLockdownWhitelist = (mLockdown && lockdownWhitelist != null) + ? Collections.unmodifiableList(new ArrayList<>(lockdownWhitelist)) + : Collections.emptyList(); + if (isCurrentPreparedPackage(packageName)) { updateAlwaysOnNotification(mNetworkInfo.getDetailedState()); + setVpnForcedLocked(mLockdown); } else { // Prepare this app. The notification will update as a side-effect of updateState(). + // It also calls setVpnForcedLocked(). prepareInternal(packageName); } - setVpnForcedLocked(mLockdown); return true; } @@ -478,13 +503,19 @@ public class Vpn { * @return the package name of the VPN controller responsible for always-on VPN, * or {@code null} if none is set or always-on VPN is controlled through * lockdown instead. - * @hide */ public synchronized String getAlwaysOnPackage() { enforceControlPermissionOrInternalCaller(); return (mAlwaysOn ? mPackage : null); } + /** + * @return an immutable list of packages whitelisted from always-on VPN lockdown. + */ + public synchronized List getLockdownWhitelist() { + return mLockdown ? mLockdownWhitelist : null; + } + /** * Save the always-on package and lockdown config into Settings.Secure */ @@ -496,6 +527,9 @@ public class Vpn { getAlwaysOnPackage(), mUserHandle); mSystemServices.settingsSecurePutIntForUser(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN, (mAlwaysOn && mLockdown ? 1 : 0), mUserHandle); + mSystemServices.settingsSecurePutStringForUser( + Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN_WHITELIST, + String.join(",", mLockdownWhitelist), mUserHandle); } finally { Binder.restoreCallingIdentity(token); } @@ -512,7 +546,11 @@ public class Vpn { Settings.Secure.ALWAYS_ON_VPN_APP, mUserHandle); final boolean alwaysOnLockdown = mSystemServices.settingsSecureGetIntForUser( Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN, 0 /*default*/, mUserHandle) != 0; - setAlwaysOnPackageInternal(alwaysOnPackage, alwaysOnLockdown); + final String whitelistString = mSystemServices.settingsSecureGetStringForUser( + Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN_WHITELIST, mUserHandle); + final List whitelistedPackages = TextUtils.isEmpty(whitelistString) + ? Collections.emptyList() : Arrays.asList(whitelistString.split(",")); + setAlwaysOnPackageInternal(alwaysOnPackage, alwaysOnLockdown, whitelistedPackages); } finally { Binder.restoreCallingIdentity(token); } @@ -532,7 +570,7 @@ public class Vpn { } // Remove always-on VPN if it's not supported. if (!isAlwaysOnPackageSupported(alwaysOnPackage)) { - setAlwaysOnPackage(null, false); + setAlwaysOnPackage(null, false, null); return false; } // Skip if the service is already established. This isn't bulletproof: it's not bound @@ -1247,9 +1285,10 @@ public class Vpn { } /** - * Restrict network access from all UIDs affected by this {@link Vpn}, apart from the VPN - * service app itself, to only sockets that have had {@code protect()} called on them. All - * non-VPN traffic is blocked via a {@code PROHIBIT} response from the kernel. + * Restricts network access from all UIDs affected by this {@link Vpn}, apart from the VPN + * service app itself and whitelisted packages, to only sockets that have had {@code protect()} + * called on them. All non-VPN traffic is blocked via a {@code PROHIBIT} response from the + * kernel. * * The exception for the VPN UID isn't technically necessary -- setup should use protected * sockets -- but in practice it saves apps that don't protect their sockets from breaking. @@ -1265,8 +1304,13 @@ public class Vpn { */ @GuardedBy("this") private void setVpnForcedLocked(boolean enforce) { - final List exemptedPackages = - isNullOrLegacyVpn(mPackage) ? null : Collections.singletonList(mPackage); + final List exemptedPackages; + if (isNullOrLegacyVpn(mPackage)) { + exemptedPackages = null; + } else { + exemptedPackages = new ArrayList<>(mLockdownWhitelist); + exemptedPackages.add(mPackage); + } final Set removedRanges = new ArraySet<>(mBlockedUsers); Set addedRanges = Collections.emptySet(); diff --git a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java index 9ca02bad50bd2..a6242e16a742a 100644 --- a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java +++ b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java @@ -16,10 +16,6 @@ package com.android.server.pm; -import com.google.android.collect.Sets; - -import com.android.internal.util.Preconditions; - import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; @@ -42,6 +38,10 @@ import android.util.Log; import android.util.Slog; import android.util.SparseArray; +import com.android.internal.util.Preconditions; + +import com.google.android.collect.Sets; + import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlSerializer; @@ -660,6 +660,7 @@ public class UserRestrictionsUtils { case android.provider.Settings.Secure.ALWAYS_ON_VPN_APP: case android.provider.Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN: + case android.provider.Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN_WHITELIST: // Whitelist system uid (ConnectivityService) and root uid to change always-on vpn final int appId = UserHandle.getAppId(callingUid); if (appId == Process.SYSTEM_UID || appId == Process.ROOT_UID) { diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 11fe76383c765..cc1dc7aa9b9c9 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -60,20 +60,14 @@ import static android.app.admin.DevicePolicyManager.WIPE_EUICC; import static android.app.admin.DevicePolicyManager.WIPE_EXTERNAL_STORAGE; import static android.app.admin.DevicePolicyManager.WIPE_RESET_PROTECTION_DATA; import static android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES; - import static android.provider.Telephony.Carriers.DPC_URI; import static android.provider.Telephony.Carriers.ENFORCE_KEY; import static android.provider.Telephony.Carriers.ENFORCE_MANAGED_URI; -import static com.android.internal.logging.nano.MetricsProto.MetricsEvent - .PROVISIONING_ENTRY_POINT_ADB; -import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker - .STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW; - +import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.PROVISIONING_ENTRY_POINT_ADB; +import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW; import static com.android.server.devicepolicy.TransferOwnershipMetadataManager.ADMIN_TYPE_DEVICE_OWNER; import static com.android.server.devicepolicy.TransferOwnershipMetadataManager.ADMIN_TYPE_PROFILE_OWNER; - - import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME; import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; @@ -219,11 +213,11 @@ import com.android.internal.util.FastXmlSerializer; import com.android.internal.util.FunctionalUtils.ThrowingRunnable; import com.android.internal.util.JournaledFile; import com.android.internal.util.Preconditions; +import com.android.internal.util.StatLogger; import com.android.internal.util.XmlUtils; import com.android.internal.widget.LockPatternUtils; import com.android.server.LocalServices; import com.android.server.LockGuard; -import com.android.internal.util.StatLogger; import com.android.server.SystemServerInitThreadPool; import com.android.server.SystemService; import com.android.server.devicepolicy.DevicePolicyManagerService.ActiveAdmin.TrustAgentInfo; @@ -1852,7 +1846,11 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } AlarmManager getAlarmManager() { - return (AlarmManager) mContext.getSystemService(AlarmManager.class); + return mContext.getSystemService(AlarmManager.class); + } + + ConnectivityManager getConnectivityManager() { + return mContext.getSystemService(ConnectivityManager.class); } IWindowManager getIWindowManager() { @@ -5877,7 +5875,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { * @throws UnsupportedOperationException if the package does not support being set as always-on. */ @Override - public boolean setAlwaysOnVpnPackage(ComponentName admin, String vpnPackage, boolean lockdown) + public boolean setAlwaysOnVpnPackage(ComponentName admin, String vpnPackage, boolean lockdown, + List lockdownWhitelist) throws SecurityException { enforceProfileOrDeviceOwner(admin); @@ -5885,11 +5884,23 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { final long token = mInjector.binderClearCallingIdentity(); try { if (vpnPackage != null && !isPackageInstalledForUser(vpnPackage, userId)) { - return false; + Slog.w(LOG_TAG, "Non-existent VPN package specified: " + vpnPackage); + throw new ServiceSpecificException( + DevicePolicyManager.ERROR_VPN_PACKAGE_NOT_FOUND, vpnPackage); } - ConnectivityManager connectivityManager = (ConnectivityManager) - mContext.getSystemService(Context.CONNECTIVITY_SERVICE); - if (!connectivityManager.setAlwaysOnVpnPackageForUser(userId, vpnPackage, lockdown)) { + + if (vpnPackage != null && lockdown && lockdownWhitelist != null) { + for (String packageName : lockdownWhitelist) { + if (!isPackageInstalledForUser(packageName, userId)) { + Slog.w(LOG_TAG, "Non-existent package in VPN whitelist: " + packageName); + throw new ServiceSpecificException( + DevicePolicyManager.ERROR_VPN_PACKAGE_NOT_FOUND, packageName); + } + } + } + // If some package is uninstalled after the check above, it will be ignored by CM. + if (!mInjector.getConnectivityManager().setAlwaysOnVpnPackageForUser( + userId, vpnPackage, lockdown, lockdownWhitelist)) { throw new UnsupportedOperationException(); } } finally { @@ -5899,16 +5910,40 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } @Override - public String getAlwaysOnVpnPackage(ComponentName admin) + public String getAlwaysOnVpnPackage(ComponentName admin) throws SecurityException { + enforceProfileOrDeviceOwner(admin); + + final int userId = mInjector.userHandleGetCallingUserId(); + final long token = mInjector.binderClearCallingIdentity(); + try { + return mInjector.getConnectivityManager().getAlwaysOnVpnPackageForUser(userId); + } finally { + mInjector.binderRestoreCallingIdentity(token); + } + } + + @Override + public boolean isAlwaysOnVpnLockdownEnabled(ComponentName admin) throws SecurityException { + enforceProfileOrDeviceOwner(admin); + + final int userId = mInjector.userHandleGetCallingUserId(); + final long token = mInjector.binderClearCallingIdentity(); + try { + return mInjector.getConnectivityManager().isVpnLockdownEnabled(userId); + } finally { + mInjector.binderRestoreCallingIdentity(token); + } + } + + @Override + public List getAlwaysOnVpnLockdownWhitelist(ComponentName admin) throws SecurityException { enforceProfileOrDeviceOwner(admin); final int userId = mInjector.userHandleGetCallingUserId(); final long token = mInjector.binderClearCallingIdentity(); - try{ - ConnectivityManager connectivityManager = (ConnectivityManager) - mContext.getSystemService(Context.CONNECTIVITY_SERVICE); - return connectivityManager.getAlwaysOnVpnPackageForUser(userId); + try { + return mInjector.getConnectivityManager().getVpnLockdownWhitelist(userId); } finally { mInjector.binderRestoreCallingIdentity(token); } @@ -6378,9 +6413,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } long token = mInjector.binderClearCallingIdentity(); try { - ConnectivityManager connectivityManager = (ConnectivityManager) - mContext.getSystemService(Context.CONNECTIVITY_SERVICE); - connectivityManager.setGlobalProxy(proxyInfo); + mInjector.getConnectivityManager().setGlobalProxy(proxyInfo); } finally { mInjector.binderRestoreCallingIdentity(token); } diff --git a/tests/net/java/com/android/server/connectivity/VpnTest.java b/tests/net/java/com/android/server/connectivity/VpnTest.java index 0b74d878f0695..5b17224e41e52 100644 --- a/tests/net/java/com/android/server/connectivity/VpnTest.java +++ b/tests/net/java/com/android/server/connectivity/VpnTest.java @@ -246,17 +246,17 @@ public class VpnTest { assertFalse(vpn.getLockdown()); // Set always-on without lockdown. - assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false)); + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false, Collections.emptyList())); assertTrue(vpn.getAlwaysOn()); assertFalse(vpn.getLockdown()); // Set always-on with lockdown. - assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true)); + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, Collections.emptyList())); assertTrue(vpn.getAlwaysOn()); assertTrue(vpn.getLockdown()); // Remove always-on configuration. - assertTrue(vpn.setAlwaysOnPackage(null, false)); + assertTrue(vpn.setAlwaysOnPackage(null, false, Collections.emptyList())); assertFalse(vpn.getAlwaysOn()); assertFalse(vpn.getLockdown()); } @@ -270,11 +270,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)); + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false, null)); 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)); + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, null)); 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) @@ -283,7 +283,7 @@ public class VpnTest { assertUnblocked(vpn, user.start + PKG_UIDS[1]); // Switch to another app. - assertTrue(vpn.setAlwaysOnPackage(PKGS[3], true)); + assertTrue(vpn.setAlwaysOnPackage(PKGS[3], true, null)); 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) @@ -296,6 +296,87 @@ public class VpnTest { assertUnblocked(vpn, user.start + PKG_UIDS[3]); } + @Test + public void testLockdownWhitelist() throws Exception { + final Vpn vpn = createVpn(primaryUser.id); + 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]))); + 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) + })); + assertBlocked(vpn, user.start + PKG_UIDS[0], user.start + PKG_UIDS[3]); + 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]))); + verify(mNetService).setAllowOnlyVpnForUids(eq(false), aryEq(new UidRange[] { + new UidRange(user.start + PKG_UIDS[2] + 1, user.stop) + })); + verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(new UidRange[] { + new UidRange(user.start + PKG_UIDS[1] + 1, user.start + PKG_UIDS[3] - 1), + new UidRange(user.start + PKG_UIDS[3] + 1, user.stop) + })); + assertBlocked(vpn, user.start + PKG_UIDS[0], user.start + PKG_UIDS[2]); + 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]))); + 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) + })); + verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(new UidRange[] { + new UidRange(user.start, user.start + PKG_UIDS[0] - 1), + new UidRange(user.start + PKG_UIDS[0] + 1, user.start + PKG_UIDS[3] - 1) + })); + assertBlocked(vpn, user.start + PKG_UIDS[1], user.start + PKG_UIDS[2]); + assertUnblocked(vpn, user.start + PKG_UIDS[0], user.start + PKG_UIDS[3]); + + // Remove the whitelist. + assertTrue(vpn.setAlwaysOnPackage(PKGS[0], true, null)); + 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) + })); + verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(new UidRange[] { + new UidRange(user.start + PKG_UIDS[0] + 1, user.stop), + })); + assertBlocked(vpn, user.start + PKG_UIDS[1], user.start + PKG_UIDS[2], + user.start + PKG_UIDS[3]); + assertUnblocked(vpn, user.start + PKG_UIDS[0]); + + // Add the whitelist. + assertTrue(vpn.setAlwaysOnPackage(PKGS[0], true, Collections.singletonList(PKGS[1]))); + verify(mNetService).setAllowOnlyVpnForUids(eq(false), aryEq(new UidRange[] { + new UidRange(user.start + PKG_UIDS[0] + 1, user.stop) + })); + verify(mNetService).setAllowOnlyVpnForUids(eq(true), 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) + })); + assertBlocked(vpn, user.start + PKG_UIDS[2], user.start + PKG_UIDS[3]); + 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"))); + + // 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"))); + 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) + })); + verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(new UidRange[]{ + new UidRange(user.start + PKG_UIDS[0] + 1, user.start + PKG_UIDS[2] - 1), + new UidRange(user.start + PKG_UIDS[2] + 1, user.stop) + })); + } + @Test public void testLockdownAddingAProfile() throws Exception { final Vpn vpn = createVpn(primaryUser.id); @@ -310,7 +391,7 @@ public class VpnTest { final UidRange profile = UidRange.createForUser(tempProfile.id); // Set lockdown. - assertTrue(vpn.setAlwaysOnPackage(PKGS[3], true)); + assertTrue(vpn.setAlwaysOnPackage(PKGS[3], true, null)); 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) @@ -436,7 +517,7 @@ public class VpnTest { .cancelAsUser(anyString(), anyInt(), eq(userHandle)); // Start showing a notification for disconnected once always-on. - vpn.setAlwaysOnPackage(PKGS[0], false); + vpn.setAlwaysOnPackage(PKGS[0], false, null); order.verify(mNotificationManager) .notifyAsUser(anyString(), anyInt(), any(), eq(userHandle)); @@ -450,7 +531,7 @@ public class VpnTest { .notifyAsUser(anyString(), anyInt(), any(), eq(userHandle)); // Notification should be cleared after unsetting always-on package. - vpn.setAlwaysOnPackage(null, false); + vpn.setAlwaysOnPackage(null, false, null); order.verify(mNotificationManager).cancelAsUser(anyString(), anyInt(), eq(userHandle)); } @@ -583,7 +664,9 @@ public class VpnTest { doAnswer(invocation -> { final String appName = (String) invocation.getArguments()[0]; final int userId = (int) invocation.getArguments()[1]; - return UserHandle.getUid(userId, packages.get(appName)); + Integer appId = packages.get(appName); + if (appId == null) throw new PackageManager.NameNotFoundException(appName); + return UserHandle.getUid(userId, appId); }).when(mPackageManager).getPackageUidAsUser(anyString(), anyInt()); } catch (Exception e) { }