diff options
| author | 2016-01-14 11:37:18 +0000 | |
|---|---|---|
| committer | 2016-01-14 11:37:18 +0000 | |
| commit | b68d2d5b68dc58fb7b75ce94af74de58a1b9d3f9 (patch) | |
| tree | 056414a933e8f09a0e7ee68f5da09f9179d9eaeb | |
| parent | 051782fd2d152d7467d7e72e5410ac1cae1597d0 (diff) | |
| parent | 244ce8ef5f201cf403bab43df8281671a9e94512 (diff) | |
Merge "Always-on app VPNs"
| -rw-r--r-- | api/current.txt | 2 | ||||
| -rw-r--r-- | api/system-current.txt | 2 | ||||
| -rw-r--r-- | api/test-current.txt | 2 | ||||
| -rw-r--r-- | core/java/android/app/admin/DevicePolicyManager.java | 47 | ||||
| -rw-r--r-- | core/java/android/app/admin/IDevicePolicyManager.aidl | 3 | ||||
| -rw-r--r-- | core/java/android/net/ConnectivityManager.java | 42 | ||||
| -rw-r--r-- | core/java/android/net/IConnectivityManager.aidl | 2 | ||||
| -rw-r--r-- | core/java/android/provider/Settings.java | 7 | ||||
| -rw-r--r-- | services/core/java/com/android/server/ConnectivityService.java | 91 | ||||
| -rw-r--r-- | services/core/java/com/android/server/connectivity/Vpn.java | 68 | ||||
| -rw-r--r-- | services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java | 36 |
11 files changed, 291 insertions, 11 deletions
diff --git a/api/current.txt b/api/current.txt index 54e5ff8a6192..358d4d5176f7 100644 --- a/api/current.txt +++ b/api/current.txt @@ -5781,6 +5781,7 @@ package android.app.admin { method public int enableSystemApp(android.content.ComponentName, android.content.Intent); method public java.lang.String[] getAccountTypesWithManagementDisabled(); method public java.util.List<android.content.ComponentName> getActiveAdmins(); + method public java.lang.String getAlwaysOnVpnPackage(android.content.ComponentName); method public android.os.Bundle getApplicationRestrictions(android.content.ComponentName, java.lang.String); method public java.lang.String getApplicationRestrictionsManagingPackage(android.content.ComponentName); method public boolean getAutoTimeRequired(); @@ -5845,6 +5846,7 @@ package android.app.admin { method public boolean removeUser(android.content.ComponentName, android.os.UserHandle); method public boolean resetPassword(java.lang.String, int); method public void setAccountManagementDisabled(android.content.ComponentName, java.lang.String, boolean); + method public boolean setAlwaysOnVpnPackage(android.content.ComponentName, java.lang.String); method public boolean setApplicationHidden(android.content.ComponentName, java.lang.String, boolean); method public void setApplicationRestrictions(android.content.ComponentName, java.lang.String, android.os.Bundle); method public void setApplicationRestrictionsManagingPackage(android.content.ComponentName, java.lang.String); diff --git a/api/system-current.txt b/api/system-current.txt index 635292dae584..9b66945ecbc9 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -5915,6 +5915,7 @@ package android.app.admin { method public int enableSystemApp(android.content.ComponentName, android.content.Intent); method public java.lang.String[] getAccountTypesWithManagementDisabled(); method public java.util.List<android.content.ComponentName> getActiveAdmins(); + method public java.lang.String getAlwaysOnVpnPackage(android.content.ComponentName); method public android.os.Bundle getApplicationRestrictions(android.content.ComponentName, java.lang.String); method public java.lang.String getApplicationRestrictionsManagingPackage(android.content.ComponentName); method public boolean getAutoTimeRequired(); @@ -5988,6 +5989,7 @@ package android.app.admin { method public boolean resetPassword(java.lang.String, int); method public void setAccountManagementDisabled(android.content.ComponentName, java.lang.String, boolean); method public deprecated boolean setActiveProfileOwner(android.content.ComponentName, java.lang.String) throws java.lang.IllegalArgumentException; + method public boolean setAlwaysOnVpnPackage(android.content.ComponentName, java.lang.String); method public boolean setApplicationHidden(android.content.ComponentName, java.lang.String, boolean); method public void setApplicationRestrictions(android.content.ComponentName, java.lang.String, android.os.Bundle); method public void setApplicationRestrictionsManagingPackage(android.content.ComponentName, java.lang.String); diff --git a/api/test-current.txt b/api/test-current.txt index 81f8b7378832..41a77b3c8a92 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -5783,6 +5783,7 @@ package android.app.admin { method public int enableSystemApp(android.content.ComponentName, android.content.Intent); method public java.lang.String[] getAccountTypesWithManagementDisabled(); method public java.util.List<android.content.ComponentName> getActiveAdmins(); + method public java.lang.String getAlwaysOnVpnPackage(android.content.ComponentName); method public android.os.Bundle getApplicationRestrictions(android.content.ComponentName, java.lang.String); method public java.lang.String getApplicationRestrictionsManagingPackage(android.content.ComponentName); method public boolean getAutoTimeRequired(); @@ -5847,6 +5848,7 @@ package android.app.admin { method public boolean removeUser(android.content.ComponentName, android.os.UserHandle); method public boolean resetPassword(java.lang.String, int); method public void setAccountManagementDisabled(android.content.ComponentName, java.lang.String, boolean); + method public boolean setAlwaysOnVpnPackage(android.content.ComponentName, java.lang.String); method public boolean setApplicationHidden(android.content.ComponentName, java.lang.String, boolean); method public void setApplicationRestrictions(android.content.ComponentName, java.lang.String, android.os.Bundle); method public void setApplicationRestrictionsManagingPackage(android.content.ComponentName, java.lang.String); diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index bebc8cfe5421..c7f11e2fe6d2 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -2451,6 +2451,53 @@ public class DevicePolicyManager { } /** + * 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. + * + * <p>The designated package should declare a {@link android.net.VpnService} in its + * manifest guarded by {@link android.Manifest.permission#BIND_VPN_SERVICE}, + * otherwise the call will fail. + * + * @param vpnPackage The package name for an installed VPN app on the device, or {@code null} + * to remove an existing always-on VPN configuration. + * + * @return {@code true} if the package is set as always-on VPN controller; + * {@code false} otherwise. + */ + public boolean setAlwaysOnVpnPackage(@NonNull ComponentName admin, + @Nullable String vpnPackage) { + if (mService != null) { + try { + return mService.setAlwaysOnVpnPackage(admin, vpnPackage); + } catch (RemoteException e) { + Log.w(TAG, REMOTE_EXCEPTION_MESSAGE, e); + } + } + return false; + } + + /** + * 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 VPN is provided by the system instead + * of by an application, {@code null} will be returned. + * + * @return Package name of VPN controller responsible for always-on VPN, + * or {@code null} if none is set. + */ + public String getAlwaysOnVpnPackage(@NonNull ComponentName admin) { + if (mService != null) { + try { + return mService.getAlwaysOnVpnPackage(admin); + } catch (RemoteException e) { + Log.w(TAG, REMOTE_EXCEPTION_MESSAGE, e); + } + } + return null; + } + + /** * Called by an application that is administering the device to disable all cameras * on the device, for this user. After setting this, no applications running as this user * will be able to access any cameras on the device. diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index 995ce0017ddc..7771440dd9f2 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -144,6 +144,9 @@ interface IDevicePolicyManager { void setCertInstallerPackage(in ComponentName who, String installerPackage); String getCertInstallerPackage(in ComponentName who); + boolean setAlwaysOnVpnPackage(in ComponentName who, String vpnPackage); + String getAlwaysOnVpnPackage(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 176e923c81b2..c4f0847e694b 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -17,6 +17,7 @@ package android.net; import static com.android.internal.util.Preconditions.checkNotNull; +import android.annotation.Nullable; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.annotation.SystemApi; @@ -686,6 +687,47 @@ public class ConnectivityManager { } /** + * Configures an always-on VPN connection through a specific application. + * This connection is automatically granted and persisted after a reboot. + * + * <p>The designated package should declare a {@link VpnService} in its + * manifest guarded by {@link android.Manifest.permission.BIND_VPN_SERVICE}, + * otherwise the call will fail. + * + * @param userId The identifier of the user to set an always-on VPN for. + * @param vpnPackage The package name for an installed VPN app on the device, or {@code null} + * to remove an existing always-on VPN configuration. + + * @return {@code true} if the package is set as always-on VPN controller; + * {@code false} otherwise. + * @hide + */ + public boolean setAlwaysOnVpnPackageForUser(int userId, @Nullable String vpnPackage) { + try { + return mService.setAlwaysOnVpnPackage(userId, vpnPackage); + } catch (RemoteException e) { + return false; + } + } + + /** + * Returns the package name of the currently set always-on VPN application. + * If there is no always-on VPN set, or the VPN is provided by the system instead + * of by an app, {@code null} will be returned. + * + * @return Package name of VPN controller responsible for always-on VPN, + * or {@code null} if none is set. + * @hide + */ + public String getAlwaysOnVpnPackageForUser(int userId) { + try { + return mService.getAlwaysOnVpnPackage(userId); + } catch (RemoteException e) { + return null; + } + } + + /** * Returns details about the currently active default data network * for a given uid. This is for internal use only to avoid spying * other apps. diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl index ef911373d4c1..569468e19687 100644 --- a/core/java/android/net/IConnectivityManager.aidl +++ b/core/java/android/net/IConnectivityManager.aidl @@ -117,6 +117,8 @@ interface IConnectivityManager VpnInfo[] getAllVpnInfo(); boolean updateLockdownVpn(); + boolean setAlwaysOnVpnPackage(int userId, String packageName); + String getAlwaysOnVpnPackage(int userId); int checkMobileProvisioning(int suggestedTimeOutMs); diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 3e06ecf37a5e..d23a92f6ea30 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -4429,6 +4429,13 @@ public final class Settings { public static final String HTTP_PROXY = Global.HTTP_PROXY; /** + * Package designated as always-on VPN provider. + * + * @hide + */ + public static final String ALWAYS_ON_VPN_APP = "always_on_vpn_app"; + + /** * Whether applications can be installed for this user via the system's * {@link Intent#ACTION_INSTALL_PACKAGE} mechanism. * diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java index 37a6c0243996..2de5324fe4e3 100644 --- a/services/core/java/com/android/server/ConnectivityService.java +++ b/services/core/java/com/android/server/ConnectivityService.java @@ -1568,12 +1568,11 @@ public class ConnectivityService extends IConnectivityManager.Stub // load the global proxy at startup mHandler.sendMessage(mHandler.obtainMessage(EVENT_APPLY_GLOBAL_HTTP_PROXY)); - // Try bringing up tracker, but if KeyStore isn't ready yet, wait - // for user to unlock device. - if (!updateLockdownVpn()) { - final IntentFilter filter = new IntentFilter(Intent.ACTION_USER_PRESENT); - mContext.registerReceiver(mUserPresentReceiver, filter); - } + // Try bringing up tracker, but KeyStore won't be ready yet for secondary users so wait + // for user to unlock device too. + updateLockdownVpn(); + final IntentFilter filter = new IntentFilter(Intent.ACTION_USER_PRESENT); + mContext.registerReceiverAsUser(mUserPresentReceiver, UserHandle.ALL, filter, null, null); // Configure whether mobile data is always on. mHandler.sendMessage(mHandler.obtainMessage(EVENT_CONFIGURE_MOBILE_DATA_ALWAYS_ON)); @@ -1586,10 +1585,16 @@ public class ConnectivityService extends IConnectivityManager.Stub private BroadcastReceiver mUserPresentReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { + // User that sent this intent = user that was just unlocked + final int unlockedUser = getSendingUserId(); + // Try creating lockdown tracker, since user present usually means // unlocked keystore. - if (updateLockdownVpn()) { - mContext.unregisterReceiver(this); + if (mUserManager.getUserInfo(unlockedUser).isPrimary() && + LockdownVpnTracker.isEnabled()) { + updateLockdownVpn(); + } else { + updateAlwaysOnVpn(unlockedUser); } } }; @@ -3258,6 +3263,76 @@ public class ConnectivityService extends IConnectivityManager.Stub } } + /** + * Sets up or tears down the always-on VPN for user {@param user} as appropriate. + * + * @return {@code false} in case of errors; {@code true} otherwise. + */ + private boolean updateAlwaysOnVpn(int user) { + final String lockdownPackage = getAlwaysOnVpnPackage(user); + if (lockdownPackage == null) { + return true; + } + + // Create an intent to start the VPN service declared in the app's manifest. + Intent serviceIntent = new Intent(VpnConfig.SERVICE_INTERFACE); + serviceIntent.setPackage(lockdownPackage); + + try { + return mContext.startServiceAsUser(serviceIntent, UserHandle.of(user)) != null; + } catch (RuntimeException e) { + return false; + } + } + + @Override + public boolean setAlwaysOnVpnPackage(int userId, String packageName) { + enforceConnectivityInternalPermission(); + enforceCrossUserPermission(userId); + + // Can't set always-on VPN if legacy VPN is already in lockdown mode. + if (LockdownVpnTracker.isEnabled()) { + return false; + } + + // If the current VPN package is the same as the new one, this is a no-op + final String oldPackage = getAlwaysOnVpnPackage(userId); + if (TextUtils.equals(oldPackage, packageName)) { + return true; + } + + synchronized (mVpns) { + Vpn vpn = mVpns.get(userId); + if (vpn == null) { + Slog.w(TAG, "User " + userId + " has no Vpn configuration"); + return false; + } + if (!vpn.setAlwaysOnPackage(packageName)) { + return false; + } + if (!updateAlwaysOnVpn(userId)) { + vpn.setAlwaysOnPackage(null); + return false; + } + } + return true; + } + + @Override + public String getAlwaysOnVpnPackage(int userId) { + enforceConnectivityInternalPermission(); + 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.getAlwaysOnPackage(); + } + } + @Override public int checkMobileProvisioning(int suggestedTimeOutMs) { // TODO: Remove? Any reason to trigger a provisioning check? diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 5bd4f987073e..e957fc685188 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -63,6 +63,7 @@ import android.os.SystemClock; import android.os.SystemService; import android.os.UserHandle; import android.os.UserManager; +import android.provider.Settings; import android.security.Credentials; import android.security.KeyStore; import android.text.TextUtils; @@ -169,6 +170,58 @@ public class Vpn { } /** + * Configures an always-on VPN connection through a specific application. + * This connection is automatically granted and persisted after a reboot. + * + * <p>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. + * + * @param newPackage the package to designate as always-on VPN supplier. + */ + public synchronized boolean setAlwaysOnPackage(String packageName) { + enforceControlPermissionOrInternalCaller(); + + // Disconnect current VPN. + prepareInternal(VpnConfig.LEGACY_VPN); + + // Pre-authorize new always-on VPN package. + if (packageName != null) { + if (!setPackageAuthorization(packageName, true)) { + return false; + } + } + + // Save the new package name in Settings.Secure. + final long token = Binder.clearCallingIdentity(); + try { + Settings.Secure.putStringForUser(mContext.getContentResolver(), + Settings.Secure.ALWAYS_ON_VPN_APP, packageName, mUserHandle); + } finally { + Binder.restoreCallingIdentity(token); + } + return true; + } + + /** + * @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(); + + final long token = Binder.clearCallingIdentity(); + try { + return Settings.Secure.getStringForUser(mContext.getContentResolver(), + Settings.Secure.ALWAYS_ON_VPN_APP, mUserHandle); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + /** * Prepare for a VPN application. This method is designed to solve * race conditions. It first compares the current prepared package * with {@code oldPackage}. If they are the same, the prepared @@ -270,14 +323,14 @@ public class Vpn { /** * Set whether a package has the ability to launch VPNs without user intervention. */ - public void setPackageAuthorization(String packageName, boolean authorized) { + public boolean setPackageAuthorization(String packageName, boolean authorized) { // Check if the caller is authorized. - enforceControlPermission(); + enforceControlPermissionOrInternalCaller(); int uid = getAppUid(packageName, mUserHandle); if (uid == -1 || VpnConfig.LEGACY_VPN.equals(packageName)) { // Authorization for nonexistent packages (or fake ones) can't be updated. - return; + return false; } long token = Binder.clearCallingIdentity(); @@ -286,11 +339,13 @@ public class Vpn { (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE); appOps.setMode(AppOpsManager.OP_ACTIVATE_VPN, uid, packageName, authorized ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED); + return true; } catch (Exception e) { Log.wtf(TAG, "Failed to set app ops for package " + packageName + ", uid " + uid, e); } finally { Binder.restoreCallingIdentity(token); } + return false; } private boolean isVpnUserPreConsented(String packageName) { @@ -743,6 +798,13 @@ public class Vpn { mContext.enforceCallingPermission(Manifest.permission.CONTROL_VPN, "Unauthorized Caller"); } + private void enforceControlPermissionOrInternalCaller() { + // Require caller to be either an application with CONTROL_VPN permission or a process + // in the system server. + mContext.enforceCallingOrSelfPermission(Manifest.permission.CONTROL_VPN, + "Unauthorized Caller"); + } + private class Connection implements ServiceConnection { private IBinder mService; diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index c4d5c50ac850..ce0474dbb5ec 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -3907,6 +3907,42 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } } + @Override + public boolean setAlwaysOnVpnPackage(ComponentName admin, String vpnPackage) + throws SecurityException { + synchronized (this) { + getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER); + } + + final int userId = mInjector.userHandleGetCallingUserId(); + final long token = mInjector.binderClearCallingIdentity(); + try{ + ConnectivityManager connectivityManager = (ConnectivityManager) + mContext.getSystemService(Context.CONNECTIVITY_SERVICE); + return connectivityManager.setAlwaysOnVpnPackageForUser(userId, vpnPackage); + } finally { + mInjector.binderRestoreCallingIdentity(token); + } + } + + @Override + public String getAlwaysOnVpnPackage(ComponentName admin) + throws SecurityException { + synchronized (this) { + getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER); + } + + final int userId = mInjector.userHandleGetCallingUserId(); + final long token = mInjector.binderClearCallingIdentity(); + try{ + ConnectivityManager connectivityManager = (ConnectivityManager) + mContext.getSystemService(Context.CONNECTIVITY_SERVICE); + return connectivityManager.getAlwaysOnVpnPackageForUser(userId); + } finally { + mInjector.binderRestoreCallingIdentity(token); + } + } + private void wipeDataLocked(boolean wipeExtRequested, String reason) { if (wipeExtRequested) { StorageManager sm = (StorageManager) mContext.getSystemService( |