Loading core/java/android/net/IConnectivityManager.aidl +8 −0 Original line number Diff line number Diff line Loading @@ -120,6 +120,14 @@ interface IConnectivityManager ParcelFileDescriptor establishVpn(in VpnConfig config); boolean provisionVpnProfile(in VpnProfile profile, String packageName); void deleteVpnProfile(String packageName); void startVpnProfile(String packageName); void stopVpnProfile(String packageName); VpnConfig getVpnConfig(int userId); @UnsupportedAppUsage Loading core/java/android/net/VpnManager.java +64 −7 Original line number Diff line number Diff line Loading @@ -20,8 +20,17 @@ import static com.android.internal.util.Preconditions.checkNotNull; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Activity; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.os.RemoteException; import com.android.internal.net.VpnProfile; import java.io.IOException; import java.security.GeneralSecurityException; /** * This class provides an interface for apps to manage platform VPN profiles Loading @@ -41,6 +50,15 @@ public class VpnManager { @NonNull private final Context mContext; @NonNull private final IConnectivityManager mService; private static Intent getIntentForConfirmation() { final Intent intent = new Intent(); final ComponentName componentName = ComponentName.unflattenFromString( Resources.getSystem().getString( com.android.internal.R.string.config_customVpnConfirmDialogComponent)); intent.setComponent(componentName); return intent; } /** * Create an instance of the VpnManger with the given context. * Loading @@ -57,18 +75,49 @@ public class VpnManager { /** * Install a VpnProfile configuration keyed on the calling app's package name. * * @param profile the PlatformVpnProfile provided by this package. Will override any previous * PlatformVpnProfile stored for this package. * @return an intent to request user consent if needed (null otherwise). * <p>This method returns {@code null} if user consent has already been granted, or an {@link * Intent} to a system activity. If an intent is returned, the application should launch the * activity using {@link Activity#startActivityForResult} to request user consent. The activity * may pop up a dialog to require user action, and the result will come back via its {@link * Activity#onActivityResult}. If the result is {@link Activity#RESULT_OK}, the user has * consented, and the VPN profile can be started. * * @param profile the VpnProfile provided by this package. Will override any previous VpnProfile * stored for this package. * @return an Intent requesting user consent to start the VPN, or null if consent is not * required based on privileges or previous user consent. */ @Nullable public Intent provisionVpnProfile(@NonNull PlatformVpnProfile profile) { throw new UnsupportedOperationException("Not yet implemented"); final VpnProfile internalProfile; try { internalProfile = profile.toVpnProfile(); } catch (GeneralSecurityException | IOException e) { // Conversion to VpnProfile failed; this is an invalid profile. Both of these exceptions // indicate a failure to convert a PrivateKey or X509Certificate to a Base64 encoded // string as required by the VpnProfile. throw new IllegalArgumentException("Failed to serialize PlatformVpnProfile", e); } try { // Profile can never be null; it either gets set, or an exception is thrown. if (mService.provisionVpnProfile(internalProfile, mContext.getOpPackageName())) { return null; } } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } return getIntentForConfirmation(); } /** Delete the VPN profile configuration that was provisioned by the calling app */ public void deleteProvisionedVpnProfile() { throw new UnsupportedOperationException("Not yet implemented"); try { mService.deleteVpnProfile(mContext.getOpPackageName()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** Loading @@ -78,11 +127,19 @@ public class VpnManager { * setup, or if user consent has not been granted */ public void startProvisionedVpnProfile() { throw new UnsupportedOperationException("Not yet implemented"); try { mService.startVpnProfile(mContext.getOpPackageName()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** Tear down the VPN provided by the calling app (if any) */ public void stopProvisionedVpnProfile() { throw new UnsupportedOperationException("Not yet implemented"); try { mService.stopVpnProfile(mContext.getOpPackageName()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } } services/core/java/com/android/server/ConnectivityService.java +80 −1 Original line number Diff line number Diff line Loading @@ -4310,7 +4310,7 @@ public class ConnectivityService extends IConnectivityManager.Stub throwIfLockdownEnabled(); Vpn vpn = mVpns.get(userId); if (vpn != null) { return vpn.prepare(oldPackage, newPackage); return vpn.prepare(oldPackage, newPackage, false); } else { return false; } Loading Loading @@ -4358,6 +4358,78 @@ public class ConnectivityService extends IConnectivityManager.Stub } } /** * Stores the given VPN profile based on the provisioning package name. * * <p>If there is already a VPN profile stored for the provisioning package, this call will * overwrite the profile. * * <p>This is designed to serve the VpnManager only; settings-based VPN profiles are managed * exclusively by the Settings app, and passed into the platform at startup time. * * @return {@code true} if user consent has already been granted, {@code false} otherwise. * @hide */ @Override public boolean provisionVpnProfile(@NonNull VpnProfile profile, @NonNull String packageName) { final int user = UserHandle.getUserId(Binder.getCallingUid()); synchronized (mVpns) { return mVpns.get(user).provisionVpnProfile(packageName, profile, mKeyStore); } } /** * Deletes the stored VPN profile for the provisioning package * * <p>If there are no profiles for the given package, this method will silently succeed. * * <p>This is designed to serve the VpnManager only; settings-based VPN profiles are managed * exclusively by the Settings app, and passed into the platform at startup time. * * @hide */ @Override public void deleteVpnProfile(@NonNull String packageName) { final int user = UserHandle.getUserId(Binder.getCallingUid()); synchronized (mVpns) { mVpns.get(user).deleteVpnProfile(packageName, mKeyStore); } } /** * Starts the VPN based on the stored profile for the given package * * <p>This is designed to serve the VpnManager only; settings-based VPN profiles are managed * exclusively by the Settings app, and passed into the platform at startup time. * * @throws IllegalArgumentException if no profile was found for the given package name. * @hide */ @Override public void startVpnProfile(@NonNull String packageName) { final int user = UserHandle.getUserId(Binder.getCallingUid()); synchronized (mVpns) { throwIfLockdownEnabled(); mVpns.get(user).startVpnProfile(packageName, mKeyStore); } } /** * Stops the Platform VPN if the provided package is running one. * * <p>This is designed to serve the VpnManager only; settings-based VPN profiles are managed * exclusively by the Settings app, and passed into the platform at startup time. * * @hide */ @Override public void stopVpnProfile(@NonNull String packageName) { final int user = UserHandle.getUserId(Binder.getCallingUid()); synchronized (mVpns) { mVpns.get(user).stopVpnProfile(packageName); } } /** * Start legacy VPN, controlling native daemons as needed. Creates a * secondary thread to perform connection work, returning quickly. Loading Loading @@ -4561,6 +4633,13 @@ public class ConnectivityService extends IConnectivityManager.Stub } } /** * Throws if there is any currently running, always-on Legacy VPN. * * <p>The LockdownVpnTracker and mLockdownEnabled both track whether an always-on Legacy VPN is * running across the entire system. Tracking for app-based VPNs is done on a per-user, * per-package basis in Vpn.java */ @GuardedBy("mVpns") private void throwIfLockdownEnabled() { if (mLockdownEnabled) { Loading services/core/java/com/android/server/connectivity/Vpn.java +199 −11 Original line number Diff line number Diff line Loading @@ -24,6 +24,8 @@ import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING; import static android.net.RouteInfo.RTN_THROW; import static android.net.RouteInfo.RTN_UNREACHABLE; import static com.android.internal.util.Preconditions.checkNotNull; import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; Loading Loading @@ -157,6 +159,16 @@ public class Vpn { // is actually O(n²)+O(n²). private static final int MAX_ROUTES_TO_EVALUATE = 150; /** * Largest profile size allowable for Platform VPNs. * * <p>The largest platform VPN profiles use IKEv2 RSA Certificate Authentication and have two * X509Certificates, and one RSAPrivateKey. This should lead to a max size of 2x 12kB for the * certificates, plus a reasonable upper bound on the private key of 32kB. The rest of the * profile is expected to be negligible in size. */ @VisibleForTesting static final int MAX_VPN_PROFILE_SIZE_BYTES = 1 << 17; // 128kB // TODO: create separate trackers for each unique VPN to support // automated reconnection Loading Loading @@ -656,6 +668,11 @@ public class Vpn { * It uses {@link VpnConfig#LEGACY_VPN} as its package name, and * it can be revoked by itself. * * The permission checks to verify that the VPN has already been granted * user consent are dependent on the type of the VPN being prepared. See * {@link AppOpsManager#OP_ACTIVATE_VPN} and {@link * AppOpsManager#OP_ACTIVATE_PLATFORM_VPN} for more information. * * Note: when we added VPN pre-consent in * https://android.googlesource.com/platform/frameworks/base/+/0554260 * the names oldPackage and newPackage became misleading, because when Loading @@ -674,10 +691,13 @@ public class Vpn { * * @param oldPackage The package name of the old VPN application * @param newPackage The package name of the new VPN application * * @param isPlatformVpn Whether the package being prepared is using a platform VPN profile. * Preparing a platform VPN profile requires only the lesser ACTIVATE_PLATFORM_VPN appop. * @return true if the operation succeeded. */ public synchronized boolean prepare(String oldPackage, String newPackage) { // TODO: Use an Intdef'd type to represent what kind of VPN the caller is preparing. public synchronized boolean prepare( String oldPackage, String newPackage, boolean isPlatformVpn) { if (oldPackage != null) { // Stop an existing always-on VPN from being dethroned by other apps. if (mAlwaysOn && !isCurrentPreparedPackage(oldPackage)) { Loading @@ -688,13 +708,14 @@ public class Vpn { if (!isCurrentPreparedPackage(oldPackage)) { // The package doesn't match. We return false (to obtain user consent) unless the // user has already consented to that VPN package. if (!oldPackage.equals(VpnConfig.LEGACY_VPN) && isVpnUserPreConsented(oldPackage)) { if (!oldPackage.equals(VpnConfig.LEGACY_VPN) && isVpnPreConsented(mContext, oldPackage, isPlatformVpn)) { prepareInternal(oldPackage); return true; } return false; } else if (!oldPackage.equals(VpnConfig.LEGACY_VPN) && !isVpnUserPreConsented(oldPackage)) { && !isVpnPreConsented(mContext, oldPackage, isPlatformVpn)) { // Currently prepared VPN is revoked, so unprepare it and return false. prepareInternal(VpnConfig.LEGACY_VPN); return false; Loading Loading @@ -805,13 +826,29 @@ public class Vpn { return false; } private boolean isVpnUserPreConsented(String packageName) { AppOpsManager appOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE); private static boolean isVpnPreConsented( Context context, String packageName, boolean isPlatformVpn) { return isPlatformVpn ? isVpnProfilePreConsented(context, packageName) : isVpnServicePreConsented(context, packageName); } // Verify that the caller matches the given package and has permission to activate VPNs. return appOps.noteOpNoThrow(AppOpsManager.OP_ACTIVATE_VPN, Binder.getCallingUid(), packageName) == AppOpsManager.MODE_ALLOWED; private static boolean doesPackageHaveAppop(Context context, String packageName, int appop) { final AppOpsManager appOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); // Verify that the caller matches the given package and has the required permission. return appOps.noteOpNoThrow(appop, Binder.getCallingUid(), packageName) == AppOpsManager.MODE_ALLOWED; } private static boolean isVpnServicePreConsented(Context context, String packageName) { return doesPackageHaveAppop(context, packageName, AppOpsManager.OP_ACTIVATE_VPN); } private static boolean isVpnProfilePreConsented(Context context, String packageName) { return doesPackageHaveAppop(context, packageName, AppOpsManager.OP_ACTIVATE_PLATFORM_VPN) || isVpnServicePreConsented(context, packageName); } private int getAppUid(String app, int userHandle) { Loading Loading @@ -1001,6 +1038,9 @@ public class Vpn { * Establish a VPN network and return the file descriptor of the VPN interface. This methods * returns {@code null} if the application is revoked or not prepared. * * <p>This method supports ONLY VpnService-based VPNs. For Platform VPNs, see {@link * provisionVpnProfile} and {@link startVpnProfile} * * @param config The parameters to configure the network. * @return The file descriptor of the VPN interface. */ Loading @@ -1011,7 +1051,7 @@ public class Vpn { return null; } // Check to ensure consent hasn't been revoked since we were prepared. if (!isVpnUserPreConsented(mPackage)) { if (!isVpnServicePreConsented(mContext, mPackage)) { return null; } // Check if the service is properly declared. Loading Loading @@ -1676,6 +1716,10 @@ public class Vpn { public int settingsSecureGetIntForUser(String key, int def, int userId) { return Settings.Secure.getIntForUser(mContext.getContentResolver(), key, def, userId); } public boolean isCallerSystem() { return Binder.getCallingUid() == Process.SYSTEM_UID; } } private native int jniCreate(int mtu); Loading Loading @@ -2224,4 +2268,148 @@ public class Vpn { } } } private void verifyCallingUidAndPackage(String packageName) { if (getAppUid(packageName, mUserHandle) != Binder.getCallingUid()) { throw new SecurityException("Mismatched package and UID"); } } @VisibleForTesting String getProfileNameForPackage(String packageName) { return Credentials.PLATFORM_VPN + mUserHandle + "_" + packageName; } /** * Stores an app-provisioned VPN profile and returns whether the app is already prepared. * * @param packageName the package name of the app provisioning this profile * @param profile the profile to be stored and provisioned * @param keyStore the System keystore instance to save VPN profiles * @returns whether or not the app has already been granted user consent */ public synchronized boolean provisionVpnProfile( @NonNull String packageName, @NonNull VpnProfile profile, @NonNull KeyStore keyStore) { checkNotNull(packageName, "No package name provided"); checkNotNull(profile, "No profile provided"); checkNotNull(keyStore, "KeyStore missing"); verifyCallingUidAndPackage(packageName); final byte[] encodedProfile = profile.encode(); if (encodedProfile.length > MAX_VPN_PROFILE_SIZE_BYTES) { throw new IllegalArgumentException("Profile too big"); } // Permissions checked during startVpnProfile() Binder.withCleanCallingIdentity( () -> { keyStore.put( getProfileNameForPackage(packageName), encodedProfile, Process.SYSTEM_UID, 0 /* flags */); }); // TODO: if package has CONTROL_VPN, grant the ACTIVATE_PLATFORM_VPN appop. // This mirrors the prepareAndAuthorize that is used by VpnService. // Return whether the app is already pre-consented return isVpnProfilePreConsented(mContext, packageName); } /** * Deletes an app-provisioned VPN profile. * * @param packageName the package name of the app provisioning this profile * @param keyStore the System keystore instance to save VPN profiles */ public synchronized void deleteVpnProfile( @NonNull String packageName, @NonNull KeyStore keyStore) { checkNotNull(packageName, "No package name provided"); checkNotNull(keyStore, "KeyStore missing"); verifyCallingUidAndPackage(packageName); Binder.withCleanCallingIdentity( () -> { keyStore.delete(getProfileNameForPackage(packageName), Process.SYSTEM_UID); }); } /** * Retrieves the VpnProfile. * * <p>Must be used only as SYSTEM_UID, otherwise the key/UID pair will not match anything in the * keystore. */ @VisibleForTesting @Nullable VpnProfile getVpnProfilePrivileged(@NonNull String packageName, @NonNull KeyStore keyStore) { if (!mSystemServices.isCallerSystem()) { Log.wtf(TAG, "getVpnProfilePrivileged called as non-System UID "); return null; } final byte[] encoded = keyStore.get(getProfileNameForPackage(packageName)); if (encoded == null) return null; return VpnProfile.decode("" /* Key unused */, encoded); } /** * Starts an already provisioned VPN Profile, keyed by package name. * * <p>This method is meant to be called by apps (via VpnManager and ConnectivityService). * Privileged (system) callers should use startVpnProfilePrivileged instead. Otherwise the UIDs * will not match during appop checks. * * @param packageName the package name of the app provisioning this profile * @param keyStore the System keystore instance to retrieve VPN profiles */ public synchronized void startVpnProfile( @NonNull String packageName, @NonNull KeyStore keyStore) { checkNotNull(packageName, "No package name provided"); checkNotNull(keyStore, "KeyStore missing"); // Prepare VPN for startup if (!prepare(packageName, null /* newPackage */, true /* isPlatformVpn */)) { throw new SecurityException("User consent not granted for package " + packageName); } Binder.withCleanCallingIdentity( () -> { final VpnProfile profile = getVpnProfilePrivileged(packageName, keyStore); if (profile == null) { throw new IllegalArgumentException("No profile found for " + packageName); } startVpnProfilePrivileged(profile); }); } private void startVpnProfilePrivileged(@NonNull VpnProfile profile) { // TODO: Start PlatformVpnRunner } /** * Stops an already running VPN Profile for the given package. * * <p>This method is meant to be called by apps (via VpnManager and ConnectivityService). * Privileged (system) callers should (re-)prepare the LEGACY_VPN instead. * * @param packageName the package name of the app provisioning this profile */ public synchronized void stopVpnProfile(@NonNull String packageName) { checkNotNull(packageName, "No package name provided"); // To stop the VPN profile, the caller must be the current prepared package. Otherwise, // the app is not prepared, and we can just return. if (!isCurrentPreparedPackage(packageName)) { // TODO: Also check to make sure that the running VPN is a VPN profile. return; } prepareInternal(VpnConfig.LEGACY_VPN); } } tests/net/java/android/net/VpnManagerTest.java +47 −19 Original line number Diff line number Diff line Loading @@ -16,13 +16,21 @@ package android.net; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.test.mock.MockContext; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.internal.net.VpnProfile; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; Loading @@ -31,7 +39,12 @@ import org.junit.runner.RunWith; @SmallTest @RunWith(AndroidJUnit4.class) public class VpnManagerTest { private static final String VPN_PROFILE_KEY = "KEY"; private static final String PKG_NAME = "fooPackage"; private static final String SESSION_NAME_STRING = "testSession"; private static final String SERVER_ADDR_STRING = "1.2.3.4"; private static final String IDENTITY_STRING = "Identity"; private static final byte[] PSK_BYTES = "preSharedKey".getBytes(); private IConnectivityManager mMockCs; private VpnManager mVpnManager; Loading @@ -39,7 +52,7 @@ public class VpnManagerTest { new MockContext() { @Override public String getOpPackageName() { return "fooPackage"; return PKG_NAME; } }; Loading @@ -50,34 +63,49 @@ public class VpnManagerTest { } @Test public void testProvisionVpnProfile() throws Exception { try { mVpnManager.provisionVpnProfile(mock(PlatformVpnProfile.class)); } catch (UnsupportedOperationException expected) { public void testProvisionVpnProfilePreconsented() throws Exception { final PlatformVpnProfile profile = getPlatformVpnProfile(); when(mMockCs.provisionVpnProfile(any(VpnProfile.class), eq(PKG_NAME))).thenReturn(true); // Expect there to be no intent returned, as consent has already been granted. assertNull(mVpnManager.provisionVpnProfile(profile)); verify(mMockCs).provisionVpnProfile(eq(profile.toVpnProfile()), eq(PKG_NAME)); } @Test public void testProvisionVpnProfileNeedsConsent() throws Exception { final PlatformVpnProfile profile = getPlatformVpnProfile(); when(mMockCs.provisionVpnProfile(any(VpnProfile.class), eq(PKG_NAME))).thenReturn(false); // Expect intent to be returned, as consent has not already been granted. assertNotNull(mVpnManager.provisionVpnProfile(profile)); verify(mMockCs).provisionVpnProfile(eq(profile.toVpnProfile()), eq(PKG_NAME)); } @Test public void testDeleteProvisionedVpnProfile() throws Exception { try { mVpnManager.deleteProvisionedVpnProfile(); } catch (UnsupportedOperationException expected) { } verify(mMockCs).deleteVpnProfile(eq(PKG_NAME)); } @Test public void testStartProvisionedVpnProfile() throws Exception { try { mVpnManager.startProvisionedVpnProfile(); } catch (UnsupportedOperationException expected) { } verify(mMockCs).startVpnProfile(eq(PKG_NAME)); } @Test public void testStopProvisionedVpnProfile() throws Exception { try { mVpnManager.stopProvisionedVpnProfile(); } catch (UnsupportedOperationException expected) { verify(mMockCs).stopVpnProfile(eq(PKG_NAME)); } private Ikev2VpnProfile getPlatformVpnProfile() throws Exception { return new Ikev2VpnProfile.Builder(SERVER_ADDR_STRING, IDENTITY_STRING) .setBypassable(true) .setMaxMtu(1300) .setMetered(true) .setAuthPsk(PSK_BYTES) .build(); } } Loading
core/java/android/net/IConnectivityManager.aidl +8 −0 Original line number Diff line number Diff line Loading @@ -120,6 +120,14 @@ interface IConnectivityManager ParcelFileDescriptor establishVpn(in VpnConfig config); boolean provisionVpnProfile(in VpnProfile profile, String packageName); void deleteVpnProfile(String packageName); void startVpnProfile(String packageName); void stopVpnProfile(String packageName); VpnConfig getVpnConfig(int userId); @UnsupportedAppUsage Loading
core/java/android/net/VpnManager.java +64 −7 Original line number Diff line number Diff line Loading @@ -20,8 +20,17 @@ import static com.android.internal.util.Preconditions.checkNotNull; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Activity; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.os.RemoteException; import com.android.internal.net.VpnProfile; import java.io.IOException; import java.security.GeneralSecurityException; /** * This class provides an interface for apps to manage platform VPN profiles Loading @@ -41,6 +50,15 @@ public class VpnManager { @NonNull private final Context mContext; @NonNull private final IConnectivityManager mService; private static Intent getIntentForConfirmation() { final Intent intent = new Intent(); final ComponentName componentName = ComponentName.unflattenFromString( Resources.getSystem().getString( com.android.internal.R.string.config_customVpnConfirmDialogComponent)); intent.setComponent(componentName); return intent; } /** * Create an instance of the VpnManger with the given context. * Loading @@ -57,18 +75,49 @@ public class VpnManager { /** * Install a VpnProfile configuration keyed on the calling app's package name. * * @param profile the PlatformVpnProfile provided by this package. Will override any previous * PlatformVpnProfile stored for this package. * @return an intent to request user consent if needed (null otherwise). * <p>This method returns {@code null} if user consent has already been granted, or an {@link * Intent} to a system activity. If an intent is returned, the application should launch the * activity using {@link Activity#startActivityForResult} to request user consent. The activity * may pop up a dialog to require user action, and the result will come back via its {@link * Activity#onActivityResult}. If the result is {@link Activity#RESULT_OK}, the user has * consented, and the VPN profile can be started. * * @param profile the VpnProfile provided by this package. Will override any previous VpnProfile * stored for this package. * @return an Intent requesting user consent to start the VPN, or null if consent is not * required based on privileges or previous user consent. */ @Nullable public Intent provisionVpnProfile(@NonNull PlatformVpnProfile profile) { throw new UnsupportedOperationException("Not yet implemented"); final VpnProfile internalProfile; try { internalProfile = profile.toVpnProfile(); } catch (GeneralSecurityException | IOException e) { // Conversion to VpnProfile failed; this is an invalid profile. Both of these exceptions // indicate a failure to convert a PrivateKey or X509Certificate to a Base64 encoded // string as required by the VpnProfile. throw new IllegalArgumentException("Failed to serialize PlatformVpnProfile", e); } try { // Profile can never be null; it either gets set, or an exception is thrown. if (mService.provisionVpnProfile(internalProfile, mContext.getOpPackageName())) { return null; } } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } return getIntentForConfirmation(); } /** Delete the VPN profile configuration that was provisioned by the calling app */ public void deleteProvisionedVpnProfile() { throw new UnsupportedOperationException("Not yet implemented"); try { mService.deleteVpnProfile(mContext.getOpPackageName()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** Loading @@ -78,11 +127,19 @@ public class VpnManager { * setup, or if user consent has not been granted */ public void startProvisionedVpnProfile() { throw new UnsupportedOperationException("Not yet implemented"); try { mService.startVpnProfile(mContext.getOpPackageName()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** Tear down the VPN provided by the calling app (if any) */ public void stopProvisionedVpnProfile() { throw new UnsupportedOperationException("Not yet implemented"); try { mService.stopVpnProfile(mContext.getOpPackageName()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } }
services/core/java/com/android/server/ConnectivityService.java +80 −1 Original line number Diff line number Diff line Loading @@ -4310,7 +4310,7 @@ public class ConnectivityService extends IConnectivityManager.Stub throwIfLockdownEnabled(); Vpn vpn = mVpns.get(userId); if (vpn != null) { return vpn.prepare(oldPackage, newPackage); return vpn.prepare(oldPackage, newPackage, false); } else { return false; } Loading Loading @@ -4358,6 +4358,78 @@ public class ConnectivityService extends IConnectivityManager.Stub } } /** * Stores the given VPN profile based on the provisioning package name. * * <p>If there is already a VPN profile stored for the provisioning package, this call will * overwrite the profile. * * <p>This is designed to serve the VpnManager only; settings-based VPN profiles are managed * exclusively by the Settings app, and passed into the platform at startup time. * * @return {@code true} if user consent has already been granted, {@code false} otherwise. * @hide */ @Override public boolean provisionVpnProfile(@NonNull VpnProfile profile, @NonNull String packageName) { final int user = UserHandle.getUserId(Binder.getCallingUid()); synchronized (mVpns) { return mVpns.get(user).provisionVpnProfile(packageName, profile, mKeyStore); } } /** * Deletes the stored VPN profile for the provisioning package * * <p>If there are no profiles for the given package, this method will silently succeed. * * <p>This is designed to serve the VpnManager only; settings-based VPN profiles are managed * exclusively by the Settings app, and passed into the platform at startup time. * * @hide */ @Override public void deleteVpnProfile(@NonNull String packageName) { final int user = UserHandle.getUserId(Binder.getCallingUid()); synchronized (mVpns) { mVpns.get(user).deleteVpnProfile(packageName, mKeyStore); } } /** * Starts the VPN based on the stored profile for the given package * * <p>This is designed to serve the VpnManager only; settings-based VPN profiles are managed * exclusively by the Settings app, and passed into the platform at startup time. * * @throws IllegalArgumentException if no profile was found for the given package name. * @hide */ @Override public void startVpnProfile(@NonNull String packageName) { final int user = UserHandle.getUserId(Binder.getCallingUid()); synchronized (mVpns) { throwIfLockdownEnabled(); mVpns.get(user).startVpnProfile(packageName, mKeyStore); } } /** * Stops the Platform VPN if the provided package is running one. * * <p>This is designed to serve the VpnManager only; settings-based VPN profiles are managed * exclusively by the Settings app, and passed into the platform at startup time. * * @hide */ @Override public void stopVpnProfile(@NonNull String packageName) { final int user = UserHandle.getUserId(Binder.getCallingUid()); synchronized (mVpns) { mVpns.get(user).stopVpnProfile(packageName); } } /** * Start legacy VPN, controlling native daemons as needed. Creates a * secondary thread to perform connection work, returning quickly. Loading Loading @@ -4561,6 +4633,13 @@ public class ConnectivityService extends IConnectivityManager.Stub } } /** * Throws if there is any currently running, always-on Legacy VPN. * * <p>The LockdownVpnTracker and mLockdownEnabled both track whether an always-on Legacy VPN is * running across the entire system. Tracking for app-based VPNs is done on a per-user, * per-package basis in Vpn.java */ @GuardedBy("mVpns") private void throwIfLockdownEnabled() { if (mLockdownEnabled) { Loading
services/core/java/com/android/server/connectivity/Vpn.java +199 −11 Original line number Diff line number Diff line Loading @@ -24,6 +24,8 @@ import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING; import static android.net.RouteInfo.RTN_THROW; import static android.net.RouteInfo.RTN_UNREACHABLE; import static com.android.internal.util.Preconditions.checkNotNull; import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; Loading Loading @@ -157,6 +159,16 @@ public class Vpn { // is actually O(n²)+O(n²). private static final int MAX_ROUTES_TO_EVALUATE = 150; /** * Largest profile size allowable for Platform VPNs. * * <p>The largest platform VPN profiles use IKEv2 RSA Certificate Authentication and have two * X509Certificates, and one RSAPrivateKey. This should lead to a max size of 2x 12kB for the * certificates, plus a reasonable upper bound on the private key of 32kB. The rest of the * profile is expected to be negligible in size. */ @VisibleForTesting static final int MAX_VPN_PROFILE_SIZE_BYTES = 1 << 17; // 128kB // TODO: create separate trackers for each unique VPN to support // automated reconnection Loading Loading @@ -656,6 +668,11 @@ public class Vpn { * It uses {@link VpnConfig#LEGACY_VPN} as its package name, and * it can be revoked by itself. * * The permission checks to verify that the VPN has already been granted * user consent are dependent on the type of the VPN being prepared. See * {@link AppOpsManager#OP_ACTIVATE_VPN} and {@link * AppOpsManager#OP_ACTIVATE_PLATFORM_VPN} for more information. * * Note: when we added VPN pre-consent in * https://android.googlesource.com/platform/frameworks/base/+/0554260 * the names oldPackage and newPackage became misleading, because when Loading @@ -674,10 +691,13 @@ public class Vpn { * * @param oldPackage The package name of the old VPN application * @param newPackage The package name of the new VPN application * * @param isPlatformVpn Whether the package being prepared is using a platform VPN profile. * Preparing a platform VPN profile requires only the lesser ACTIVATE_PLATFORM_VPN appop. * @return true if the operation succeeded. */ public synchronized boolean prepare(String oldPackage, String newPackage) { // TODO: Use an Intdef'd type to represent what kind of VPN the caller is preparing. public synchronized boolean prepare( String oldPackage, String newPackage, boolean isPlatformVpn) { if (oldPackage != null) { // Stop an existing always-on VPN from being dethroned by other apps. if (mAlwaysOn && !isCurrentPreparedPackage(oldPackage)) { Loading @@ -688,13 +708,14 @@ public class Vpn { if (!isCurrentPreparedPackage(oldPackage)) { // The package doesn't match. We return false (to obtain user consent) unless the // user has already consented to that VPN package. if (!oldPackage.equals(VpnConfig.LEGACY_VPN) && isVpnUserPreConsented(oldPackage)) { if (!oldPackage.equals(VpnConfig.LEGACY_VPN) && isVpnPreConsented(mContext, oldPackage, isPlatformVpn)) { prepareInternal(oldPackage); return true; } return false; } else if (!oldPackage.equals(VpnConfig.LEGACY_VPN) && !isVpnUserPreConsented(oldPackage)) { && !isVpnPreConsented(mContext, oldPackage, isPlatformVpn)) { // Currently prepared VPN is revoked, so unprepare it and return false. prepareInternal(VpnConfig.LEGACY_VPN); return false; Loading Loading @@ -805,13 +826,29 @@ public class Vpn { return false; } private boolean isVpnUserPreConsented(String packageName) { AppOpsManager appOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE); private static boolean isVpnPreConsented( Context context, String packageName, boolean isPlatformVpn) { return isPlatformVpn ? isVpnProfilePreConsented(context, packageName) : isVpnServicePreConsented(context, packageName); } // Verify that the caller matches the given package and has permission to activate VPNs. return appOps.noteOpNoThrow(AppOpsManager.OP_ACTIVATE_VPN, Binder.getCallingUid(), packageName) == AppOpsManager.MODE_ALLOWED; private static boolean doesPackageHaveAppop(Context context, String packageName, int appop) { final AppOpsManager appOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); // Verify that the caller matches the given package and has the required permission. return appOps.noteOpNoThrow(appop, Binder.getCallingUid(), packageName) == AppOpsManager.MODE_ALLOWED; } private static boolean isVpnServicePreConsented(Context context, String packageName) { return doesPackageHaveAppop(context, packageName, AppOpsManager.OP_ACTIVATE_VPN); } private static boolean isVpnProfilePreConsented(Context context, String packageName) { return doesPackageHaveAppop(context, packageName, AppOpsManager.OP_ACTIVATE_PLATFORM_VPN) || isVpnServicePreConsented(context, packageName); } private int getAppUid(String app, int userHandle) { Loading Loading @@ -1001,6 +1038,9 @@ public class Vpn { * Establish a VPN network and return the file descriptor of the VPN interface. This methods * returns {@code null} if the application is revoked or not prepared. * * <p>This method supports ONLY VpnService-based VPNs. For Platform VPNs, see {@link * provisionVpnProfile} and {@link startVpnProfile} * * @param config The parameters to configure the network. * @return The file descriptor of the VPN interface. */ Loading @@ -1011,7 +1051,7 @@ public class Vpn { return null; } // Check to ensure consent hasn't been revoked since we were prepared. if (!isVpnUserPreConsented(mPackage)) { if (!isVpnServicePreConsented(mContext, mPackage)) { return null; } // Check if the service is properly declared. Loading Loading @@ -1676,6 +1716,10 @@ public class Vpn { public int settingsSecureGetIntForUser(String key, int def, int userId) { return Settings.Secure.getIntForUser(mContext.getContentResolver(), key, def, userId); } public boolean isCallerSystem() { return Binder.getCallingUid() == Process.SYSTEM_UID; } } private native int jniCreate(int mtu); Loading Loading @@ -2224,4 +2268,148 @@ public class Vpn { } } } private void verifyCallingUidAndPackage(String packageName) { if (getAppUid(packageName, mUserHandle) != Binder.getCallingUid()) { throw new SecurityException("Mismatched package and UID"); } } @VisibleForTesting String getProfileNameForPackage(String packageName) { return Credentials.PLATFORM_VPN + mUserHandle + "_" + packageName; } /** * Stores an app-provisioned VPN profile and returns whether the app is already prepared. * * @param packageName the package name of the app provisioning this profile * @param profile the profile to be stored and provisioned * @param keyStore the System keystore instance to save VPN profiles * @returns whether or not the app has already been granted user consent */ public synchronized boolean provisionVpnProfile( @NonNull String packageName, @NonNull VpnProfile profile, @NonNull KeyStore keyStore) { checkNotNull(packageName, "No package name provided"); checkNotNull(profile, "No profile provided"); checkNotNull(keyStore, "KeyStore missing"); verifyCallingUidAndPackage(packageName); final byte[] encodedProfile = profile.encode(); if (encodedProfile.length > MAX_VPN_PROFILE_SIZE_BYTES) { throw new IllegalArgumentException("Profile too big"); } // Permissions checked during startVpnProfile() Binder.withCleanCallingIdentity( () -> { keyStore.put( getProfileNameForPackage(packageName), encodedProfile, Process.SYSTEM_UID, 0 /* flags */); }); // TODO: if package has CONTROL_VPN, grant the ACTIVATE_PLATFORM_VPN appop. // This mirrors the prepareAndAuthorize that is used by VpnService. // Return whether the app is already pre-consented return isVpnProfilePreConsented(mContext, packageName); } /** * Deletes an app-provisioned VPN profile. * * @param packageName the package name of the app provisioning this profile * @param keyStore the System keystore instance to save VPN profiles */ public synchronized void deleteVpnProfile( @NonNull String packageName, @NonNull KeyStore keyStore) { checkNotNull(packageName, "No package name provided"); checkNotNull(keyStore, "KeyStore missing"); verifyCallingUidAndPackage(packageName); Binder.withCleanCallingIdentity( () -> { keyStore.delete(getProfileNameForPackage(packageName), Process.SYSTEM_UID); }); } /** * Retrieves the VpnProfile. * * <p>Must be used only as SYSTEM_UID, otherwise the key/UID pair will not match anything in the * keystore. */ @VisibleForTesting @Nullable VpnProfile getVpnProfilePrivileged(@NonNull String packageName, @NonNull KeyStore keyStore) { if (!mSystemServices.isCallerSystem()) { Log.wtf(TAG, "getVpnProfilePrivileged called as non-System UID "); return null; } final byte[] encoded = keyStore.get(getProfileNameForPackage(packageName)); if (encoded == null) return null; return VpnProfile.decode("" /* Key unused */, encoded); } /** * Starts an already provisioned VPN Profile, keyed by package name. * * <p>This method is meant to be called by apps (via VpnManager and ConnectivityService). * Privileged (system) callers should use startVpnProfilePrivileged instead. Otherwise the UIDs * will not match during appop checks. * * @param packageName the package name of the app provisioning this profile * @param keyStore the System keystore instance to retrieve VPN profiles */ public synchronized void startVpnProfile( @NonNull String packageName, @NonNull KeyStore keyStore) { checkNotNull(packageName, "No package name provided"); checkNotNull(keyStore, "KeyStore missing"); // Prepare VPN for startup if (!prepare(packageName, null /* newPackage */, true /* isPlatformVpn */)) { throw new SecurityException("User consent not granted for package " + packageName); } Binder.withCleanCallingIdentity( () -> { final VpnProfile profile = getVpnProfilePrivileged(packageName, keyStore); if (profile == null) { throw new IllegalArgumentException("No profile found for " + packageName); } startVpnProfilePrivileged(profile); }); } private void startVpnProfilePrivileged(@NonNull VpnProfile profile) { // TODO: Start PlatformVpnRunner } /** * Stops an already running VPN Profile for the given package. * * <p>This method is meant to be called by apps (via VpnManager and ConnectivityService). * Privileged (system) callers should (re-)prepare the LEGACY_VPN instead. * * @param packageName the package name of the app provisioning this profile */ public synchronized void stopVpnProfile(@NonNull String packageName) { checkNotNull(packageName, "No package name provided"); // To stop the VPN profile, the caller must be the current prepared package. Otherwise, // the app is not prepared, and we can just return. if (!isCurrentPreparedPackage(packageName)) { // TODO: Also check to make sure that the running VPN is a VPN profile. return; } prepareInternal(VpnConfig.LEGACY_VPN); } }
tests/net/java/android/net/VpnManagerTest.java +47 −19 Original line number Diff line number Diff line Loading @@ -16,13 +16,21 @@ package android.net; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.test.mock.MockContext; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.internal.net.VpnProfile; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; Loading @@ -31,7 +39,12 @@ import org.junit.runner.RunWith; @SmallTest @RunWith(AndroidJUnit4.class) public class VpnManagerTest { private static final String VPN_PROFILE_KEY = "KEY"; private static final String PKG_NAME = "fooPackage"; private static final String SESSION_NAME_STRING = "testSession"; private static final String SERVER_ADDR_STRING = "1.2.3.4"; private static final String IDENTITY_STRING = "Identity"; private static final byte[] PSK_BYTES = "preSharedKey".getBytes(); private IConnectivityManager mMockCs; private VpnManager mVpnManager; Loading @@ -39,7 +52,7 @@ public class VpnManagerTest { new MockContext() { @Override public String getOpPackageName() { return "fooPackage"; return PKG_NAME; } }; Loading @@ -50,34 +63,49 @@ public class VpnManagerTest { } @Test public void testProvisionVpnProfile() throws Exception { try { mVpnManager.provisionVpnProfile(mock(PlatformVpnProfile.class)); } catch (UnsupportedOperationException expected) { public void testProvisionVpnProfilePreconsented() throws Exception { final PlatformVpnProfile profile = getPlatformVpnProfile(); when(mMockCs.provisionVpnProfile(any(VpnProfile.class), eq(PKG_NAME))).thenReturn(true); // Expect there to be no intent returned, as consent has already been granted. assertNull(mVpnManager.provisionVpnProfile(profile)); verify(mMockCs).provisionVpnProfile(eq(profile.toVpnProfile()), eq(PKG_NAME)); } @Test public void testProvisionVpnProfileNeedsConsent() throws Exception { final PlatformVpnProfile profile = getPlatformVpnProfile(); when(mMockCs.provisionVpnProfile(any(VpnProfile.class), eq(PKG_NAME))).thenReturn(false); // Expect intent to be returned, as consent has not already been granted. assertNotNull(mVpnManager.provisionVpnProfile(profile)); verify(mMockCs).provisionVpnProfile(eq(profile.toVpnProfile()), eq(PKG_NAME)); } @Test public void testDeleteProvisionedVpnProfile() throws Exception { try { mVpnManager.deleteProvisionedVpnProfile(); } catch (UnsupportedOperationException expected) { } verify(mMockCs).deleteVpnProfile(eq(PKG_NAME)); } @Test public void testStartProvisionedVpnProfile() throws Exception { try { mVpnManager.startProvisionedVpnProfile(); } catch (UnsupportedOperationException expected) { } verify(mMockCs).startVpnProfile(eq(PKG_NAME)); } @Test public void testStopProvisionedVpnProfile() throws Exception { try { mVpnManager.stopProvisionedVpnProfile(); } catch (UnsupportedOperationException expected) { verify(mMockCs).stopVpnProfile(eq(PKG_NAME)); } private Ikev2VpnProfile getPlatformVpnProfile() throws Exception { return new Ikev2VpnProfile.Builder(SERVER_ADDR_STRING, IDENTITY_STRING) .setBypassable(true) .setMaxMtu(1300) .setMetered(true) .setAuthPsk(PSK_BYTES) .build(); } }