diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index 176e923c81..c4f0847e69 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; @@ -685,6 +686,47 @@ public class ConnectivityManager { } } + /** + * Configures an always-on VPN connection through a specific application. + * This connection is automatically granted and persisted after a reboot. + * + *
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 diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl index ef911373d4..569468e196 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/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java index 37a6c02439..2de5324fe4 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?