summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Robin Lee <rgl@google.com> 2016-01-14 11:37:18 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2016-01-14 11:37:18 +0000
commitb68d2d5b68dc58fb7b75ce94af74de58a1b9d3f9 (patch)
tree056414a933e8f09a0e7ee68f5da09f9179d9eaeb
parent051782fd2d152d7467d7e72e5410ac1cae1597d0 (diff)
parent244ce8ef5f201cf403bab43df8281671a9e94512 (diff)
Merge "Always-on app VPNs"
-rw-r--r--api/current.txt2
-rw-r--r--api/system-current.txt2
-rw-r--r--api/test-current.txt2
-rw-r--r--core/java/android/app/admin/DevicePolicyManager.java47
-rw-r--r--core/java/android/app/admin/IDevicePolicyManager.aidl3
-rw-r--r--core/java/android/net/ConnectivityManager.java42
-rw-r--r--core/java/android/net/IConnectivityManager.aidl2
-rw-r--r--core/java/android/provider/Settings.java7
-rw-r--r--services/core/java/com/android/server/ConnectivityService.java91
-rw-r--r--services/core/java/com/android/server/connectivity/Vpn.java68
-rw-r--r--services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java36
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(