Merge changes I446a8595,I68d2293f am: 29044acc20 am: d7ab8fec8c
Change-Id: I27a4344fa2e3b91fe31e02bf41f20a2bc11c2ff6
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -631,6 +638,185 @@ public class VpnTest {
|
||||
assertTrue(caps.hasCapability(NET_CAPABILITY_NOT_CONGESTED));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user