diff options
| author | 2020-02-03 22:34:25 +0000 | |
|---|---|---|
| committer | 2020-02-03 22:34:25 +0000 | |
| commit | d7ab8fec8c7f72448d8d174c2c78014a243dbb8c (patch) | |
| tree | 6936560985bbc8c21b3304a52bc2976b7781e5f9 | |
| parent | a2cf5100f0d0b0e065cac9486eace937eaec1c48 (diff) | |
| parent | 29044acc206e6e1d47d54121886965af0c187f41 (diff) | |
Merge changes I446a8595,I68d2293f am: 29044acc20
Change-Id: I8422163249ca637ab71b71777feded76e3225c2e
| -rw-r--r-- | core/java/android/net/IConnectivityManager.aidl | 8 | ||||
| -rw-r--r-- | core/java/android/net/VpnManager.java | 71 | ||||
| -rw-r--r-- | services/core/java/com/android/server/ConnectivityService.java | 81 | ||||
| -rw-r--r-- | services/core/java/com/android/server/connectivity/Vpn.java | 210 | ||||
| -rw-r--r-- | tests/net/java/android/net/VpnManagerTest.java | 66 | ||||
| -rw-r--r-- | tests/net/java/com/android/server/connectivity/VpnTest.java | 206 |
6 files changed, 594 insertions, 48 deletions
diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl index 3e9e7faccb02..b050e479d2b6 100644 --- a/core/java/android/net/IConnectivityManager.aidl +++ b/core/java/android/net/IConnectivityManager.aidl @@ -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 diff --git a/core/java/android/net/VpnManager.java b/core/java/android/net/VpnManager.java index f95807a14f00..e60cc81bf9d2 100644 --- a/core/java/android/net/VpnManager.java +++ b/core/java/android/net/VpnManager.java @@ -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 @@ -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. * @@ -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(); + } } /** @@ -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(); + } } } diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java index 51f6ab0def3e..4b9925fe597b 100644 --- a/services/core/java/com/android/server/ConnectivityService.java +++ b/services/core/java/com/android/server/ConnectivityService.java @@ -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; } @@ -4359,6 +4359,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. */ @@ -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) { diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 2933fab465e5..eabc08388cff 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -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; @@ -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 @@ -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 @@ -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)) { @@ -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; @@ -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) { @@ -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. */ @@ -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. @@ -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); @@ -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); + } } diff --git a/tests/net/java/android/net/VpnManagerTest.java b/tests/net/java/android/net/VpnManagerTest.java index 655c4d118592..97551c94e2ae 100644 --- a/tests/net/java/android/net/VpnManagerTest.java +++ b/tests/net/java/android/net/VpnManagerTest.java @@ -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; @@ -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; @@ -39,7 +52,7 @@ public class VpnManagerTest { new MockContext() { @Override public String getOpPackageName() { - return "fooPackage"; + return PKG_NAME; } }; @@ -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) { - } + mVpnManager.deleteProvisionedVpnProfile(); + verify(mMockCs).deleteVpnProfile(eq(PKG_NAME)); } @Test public void testStartProvisionedVpnProfile() throws Exception { - try { - mVpnManager.startProvisionedVpnProfile(); - } catch (UnsupportedOperationException expected) { - } + mVpnManager.startProvisionedVpnProfile(); + verify(mMockCs).startVpnProfile(eq(PKG_NAME)); } @Test public void testStopProvisionedVpnProfile() throws Exception { - try { - mVpnManager.stopProvisionedVpnProfile(); - } catch (UnsupportedOperationException expected) { - } + mVpnManager.stopProvisionedVpnProfile(); + 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(); } } diff --git a/tests/net/java/com/android/server/connectivity/VpnTest.java b/tests/net/java/com/android/server/connectivity/VpnTest.java index ce50bef53d75..084ec7330236 100644 --- a/tests/net/java/com/android/server/connectivity/VpnTest.java +++ b/tests/net/java/com/android/server/connectivity/VpnTest.java @@ -28,11 +28,11 @@ import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; import static android.net.NetworkCapabilities.TRANSPORT_VPN; import static android.net.NetworkCapabilities.TRANSPORT_WIFI; -import static android.net.RouteInfo.RTN_UNREACHABLE; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.AdditionalMatchers.aryEq; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -43,6 +43,7 @@ import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -58,21 +59,20 @@ import android.content.pm.ServiceInfo; import android.content.pm.UserInfo; import android.content.res.Resources; import android.net.ConnectivityManager; -import android.net.IpPrefix; -import android.net.LinkProperties; import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkInfo.DetailedState; -import android.net.RouteInfo; import android.net.UidRange; import android.net.VpnService; import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.os.INetworkManagementService; import android.os.Looper; -import android.os.SystemClock; +import android.os.Process; import android.os.UserHandle; import android.os.UserManager; +import android.security.Credentials; +import android.security.KeyStore; import android.util.ArrayMap; import android.util.ArraySet; @@ -81,6 +81,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.internal.R; import com.android.internal.net.VpnConfig; +import com.android.internal.net.VpnProfile; import org.junit.Before; import org.junit.Test; @@ -90,9 +91,6 @@ import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import java.net.Inet4Address; -import java.net.Inet6Address; -import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -124,6 +122,8 @@ public class VpnTest { managedProfileA.profileGroupId = primaryUser.id; } + static final String TEST_VPN_PKG = "com.dummy.vpn"; + /** * Names and UIDs for some fake packages. Important points: * - UID is ordered increasing. @@ -148,6 +148,8 @@ public class VpnTest { @Mock private NotificationManager mNotificationManager; @Mock private Vpn.SystemServices mSystemServices; @Mock private ConnectivityManager mConnectivityManager; + @Mock private KeyStore mKeyStore; + private final VpnProfile mVpnProfile = new VpnProfile("key"); @Before public void setUp() throws Exception { @@ -166,6 +168,7 @@ public class VpnTest { when(mContext.getString(R.string.config_customVpnAlwaysOnDisconnectedDialogComponent)) .thenReturn(Resources.getSystem().getString( R.string.config_customVpnAlwaysOnDisconnectedDialogComponent)); + when(mSystemServices.isCallerSystem()).thenReturn(true); // Used by {@link Notification.Builder} ApplicationInfo applicationInfo = new ApplicationInfo(); @@ -175,6 +178,10 @@ public class VpnTest { .thenReturn(applicationInfo); doNothing().when(mNetService).registerObserver(any()); + + // Deny all appops by default. + when(mAppOps.noteOpNoThrow(anyInt(), anyInt(), anyString())) + .thenReturn(AppOpsManager.MODE_IGNORED); } @Test @@ -464,12 +471,12 @@ public class VpnTest { order.verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(entireUser)); // When a new VPN package is set the rules should change to cover that package. - vpn.prepare(null, PKGS[0]); + vpn.prepare(null, PKGS[0], false /* isPlatformVpn */); order.verify(mNetService).setAllowOnlyVpnForUids(eq(false), aryEq(entireUser)); order.verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(exceptPkg0)); // When that VPN package is unset, everything should be undone again in reverse. - vpn.prepare(null, VpnConfig.LEGACY_VPN); + vpn.prepare(null, VpnConfig.LEGACY_VPN, false /* isPlatformVpn */); order.verify(mNetService).setAllowOnlyVpnForUids(eq(false), aryEq(exceptPkg0)); order.verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(entireUser)); } @@ -632,6 +639,185 @@ public class VpnTest { } /** + * The profile name should NOT change between releases for backwards compatibility + * + * <p>If this is changed between releases, the {@link Vpn#getVpnProfilePrivileged()} method MUST + * be updated to ensure backward compatibility. + */ + @Test + public void testGetProfileNameForPackage() throws Exception { + final Vpn vpn = createVpn(primaryUser.id); + setMockedUsers(primaryUser); + + final String expected = Credentials.PLATFORM_VPN + primaryUser.id + "_" + TEST_VPN_PKG; + assertEquals(expected, vpn.getProfileNameForPackage(TEST_VPN_PKG)); + } + + private Vpn createVpnAndSetupUidChecks(int... grantedOps) throws Exception { + final Vpn vpn = createVpn(primaryUser.id); + setMockedUsers(primaryUser); + + when(mPackageManager.getPackageUidAsUser(eq(TEST_VPN_PKG), anyInt())) + .thenReturn(Process.myUid()); + + for (final int op : grantedOps) { + when(mAppOps.noteOpNoThrow(op, Process.myUid(), TEST_VPN_PKG)) + .thenReturn(AppOpsManager.MODE_ALLOWED); + } + + return vpn; + } + + private void checkProvisionVpnProfile(Vpn vpn, boolean expectedResult, int... checkedOps) { + assertEquals(expectedResult, vpn.provisionVpnProfile(TEST_VPN_PKG, mVpnProfile, mKeyStore)); + + // The profile should always be stored, whether or not consent has been previously granted. + verify(mKeyStore) + .put( + eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)), + eq(mVpnProfile.encode()), + eq(Process.SYSTEM_UID), + eq(0)); + + for (final int checkedOp : checkedOps) { + verify(mAppOps).noteOpNoThrow(checkedOp, Process.myUid(), TEST_VPN_PKG); + } + } + + @Test + public void testProvisionVpnProfilePreconsented() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN); + + checkProvisionVpnProfile( + vpn, true /* expectedResult */, AppOpsManager.OP_ACTIVATE_PLATFORM_VPN); + } + + @Test + public void testProvisionVpnProfileNotPreconsented() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(); + + // Expect that both the ACTIVATE_VPN and ACTIVATE_PLATFORM_VPN were tried, but the caller + // had neither. + checkProvisionVpnProfile(vpn, false /* expectedResult */, + AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, AppOpsManager.OP_ACTIVATE_VPN); + } + + @Test + public void testProvisionVpnProfileVpnServicePreconsented() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OP_ACTIVATE_VPN); + + checkProvisionVpnProfile(vpn, true /* expectedResult */, AppOpsManager.OP_ACTIVATE_VPN); + } + + @Test + public void testProvisionVpnProfileTooLarge() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN); + + final VpnProfile bigProfile = new VpnProfile(""); + bigProfile.name = new String(new byte[Vpn.MAX_VPN_PROFILE_SIZE_BYTES + 1]); + + try { + vpn.provisionVpnProfile(TEST_VPN_PKG, bigProfile, mKeyStore); + fail("Expected IAE due to profile size"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testDeleteVpnProfile() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(); + + vpn.deleteVpnProfile(TEST_VPN_PKG, mKeyStore); + + verify(mKeyStore) + .delete(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)), eq(Process.SYSTEM_UID)); + } + + @Test + public void testGetVpnProfilePrivileged() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(); + + when(mKeyStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) + .thenReturn(new VpnProfile("").encode()); + + vpn.getVpnProfilePrivileged(TEST_VPN_PKG, mKeyStore); + + verify(mKeyStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG))); + } + + @Test + public void testStartVpnProfile() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN); + + when(mKeyStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) + .thenReturn(mVpnProfile.encode()); + + vpn.startVpnProfile(TEST_VPN_PKG, mKeyStore); + + verify(mKeyStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG))); + verify(mAppOps) + .noteOpNoThrow( + eq(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN), + eq(Process.myUid()), + eq(TEST_VPN_PKG)); + } + + @Test + public void testStartVpnProfileVpnServicePreconsented() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OP_ACTIVATE_VPN); + + when(mKeyStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) + .thenReturn(mVpnProfile.encode()); + + vpn.startVpnProfile(TEST_VPN_PKG, mKeyStore); + + // Verify that the the ACTIVATE_VPN appop was checked, but no error was thrown. + verify(mAppOps).noteOpNoThrow(AppOpsManager.OP_ACTIVATE_VPN, Process.myUid(), TEST_VPN_PKG); + } + + @Test + public void testStartVpnProfileNotConsented() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(); + + try { + vpn.startVpnProfile(TEST_VPN_PKG, mKeyStore); + fail("Expected failure due to no user consent"); + } catch (SecurityException expected) { + } + + // Verify both appops were checked. + verify(mAppOps) + .noteOpNoThrow( + eq(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN), + eq(Process.myUid()), + eq(TEST_VPN_PKG)); + verify(mAppOps).noteOpNoThrow(AppOpsManager.OP_ACTIVATE_VPN, Process.myUid(), TEST_VPN_PKG); + + // Keystore should never have been accessed. + verify(mKeyStore, never()).get(any()); + } + + @Test + public void testStartVpnProfileMissingProfile() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN); + + when(mKeyStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))).thenReturn(null); + + try { + vpn.startVpnProfile(TEST_VPN_PKG, mKeyStore); + fail("Expected failure due to missing profile"); + } catch (IllegalArgumentException expected) { + } + + verify(mKeyStore).get(vpn.getProfileNameForPackage(TEST_VPN_PKG)); + verify(mAppOps) + .noteOpNoThrow( + eq(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN), + eq(Process.myUid()), + eq(TEST_VPN_PKG)); + } + + /** * Mock some methods of vpn object. */ private Vpn createVpn(@UserIdInt int userId) { |