Add basic logic for profile-based VPNs

This change adds stubs for the Platform built-in VPNs, along with
implementing some basic permissions checks.

Bug: 144246837
Test: FrameworksNetTests passing, new tests added
Change-Id: I68d2293fc1468544f0d9f64d02ea7e1c80c8d18c
This commit is contained in:
Benedict Wong
2019-11-05 12:56:25 -08:00
parent 055202128f
commit df936cf1a7
3 changed files with 284 additions and 11 deletions

View File

@@ -120,6 +120,14 @@ interface IConnectivityManager
ParcelFileDescriptor establishVpn(in VpnConfig config); 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); VpnConfig getVpnConfig(int userId);
@UnsupportedAppUsage @UnsupportedAppUsage

View File

@@ -4293,7 +4293,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
throwIfLockdownEnabled(); throwIfLockdownEnabled();
Vpn vpn = mVpns.get(userId); Vpn vpn = mVpns.get(userId);
if (vpn != null) { if (vpn != null) {
return vpn.prepare(oldPackage, newPackage); return vpn.prepare(oldPackage, newPackage, false);
} else { } else {
return false; return false;
} }
@@ -4341,6 +4341,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 * Start legacy VPN, controlling native daemons as needed. Creates a
* secondary thread to perform connection work, returning quickly. * secondary thread to perform connection work, returning quickly.
@@ -4544,6 +4616,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") @GuardedBy("mVpns")
private void throwIfLockdownEnabled() { private void throwIfLockdownEnabled() {
if (mLockdownEnabled) { if (mLockdownEnabled) {

View File

@@ -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_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_VPN; import static android.net.NetworkCapabilities.TRANSPORT_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI; 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.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.AdditionalMatchers.aryEq; import static org.mockito.AdditionalMatchers.aryEq;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean; 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.doAnswer;
import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -58,21 +59,20 @@ import android.content.pm.ServiceInfo;
import android.content.pm.UserInfo; import android.content.pm.UserInfo;
import android.content.res.Resources; import android.content.res.Resources;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
import android.net.IpPrefix;
import android.net.LinkProperties;
import android.net.Network; import android.net.Network;
import android.net.NetworkCapabilities; import android.net.NetworkCapabilities;
import android.net.NetworkInfo.DetailedState; import android.net.NetworkInfo.DetailedState;
import android.net.RouteInfo;
import android.net.UidRange; import android.net.UidRange;
import android.net.VpnService; import android.net.VpnService;
import android.os.Build.VERSION_CODES; import android.os.Build.VERSION_CODES;
import android.os.Bundle; import android.os.Bundle;
import android.os.INetworkManagementService; import android.os.INetworkManagementService;
import android.os.Looper; import android.os.Looper;
import android.os.SystemClock; import android.os.Process;
import android.os.UserHandle; import android.os.UserHandle;
import android.os.UserManager; import android.os.UserManager;
import android.security.Credentials;
import android.security.KeyStore;
import android.util.ArrayMap; import android.util.ArrayMap;
import android.util.ArraySet; import android.util.ArraySet;
@@ -81,6 +81,7 @@ import androidx.test.runner.AndroidJUnit4;
import com.android.internal.R; import com.android.internal.R;
import com.android.internal.net.VpnConfig; import com.android.internal.net.VpnConfig;
import com.android.internal.net.VpnProfile;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@@ -90,9 +91,6 @@ import org.mockito.InOrder;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.UnknownHostException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@@ -124,6 +122,8 @@ public class VpnTest {
managedProfileA.profileGroupId = primaryUser.id; managedProfileA.profileGroupId = primaryUser.id;
} }
static final String TEST_VPN_PKG = "com.dummy.vpn";
/** /**
* Names and UIDs for some fake packages. Important points: * Names and UIDs for some fake packages. Important points:
* - UID is ordered increasing. * - UID is ordered increasing.
@@ -148,6 +148,8 @@ public class VpnTest {
@Mock private NotificationManager mNotificationManager; @Mock private NotificationManager mNotificationManager;
@Mock private Vpn.SystemServices mSystemServices; @Mock private Vpn.SystemServices mSystemServices;
@Mock private ConnectivityManager mConnectivityManager; @Mock private ConnectivityManager mConnectivityManager;
@Mock private KeyStore mKeyStore;
private final VpnProfile mVpnProfile = new VpnProfile("key");
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
@@ -166,6 +168,7 @@ public class VpnTest {
when(mContext.getString(R.string.config_customVpnAlwaysOnDisconnectedDialogComponent)) when(mContext.getString(R.string.config_customVpnAlwaysOnDisconnectedDialogComponent))
.thenReturn(Resources.getSystem().getString( .thenReturn(Resources.getSystem().getString(
R.string.config_customVpnAlwaysOnDisconnectedDialogComponent)); R.string.config_customVpnAlwaysOnDisconnectedDialogComponent));
when(mSystemServices.isCallerSystem()).thenReturn(true);
// Used by {@link Notification.Builder} // Used by {@link Notification.Builder}
ApplicationInfo applicationInfo = new ApplicationInfo(); ApplicationInfo applicationInfo = new ApplicationInfo();
@@ -175,6 +178,10 @@ public class VpnTest {
.thenReturn(applicationInfo); .thenReturn(applicationInfo);
doNothing().when(mNetService).registerObserver(any()); doNothing().when(mNetService).registerObserver(any());
// Deny all appops by default.
when(mAppOps.noteOpNoThrow(anyInt(), anyInt(), anyString()))
.thenReturn(AppOpsManager.MODE_IGNORED);
} }
@Test @Test
@@ -464,12 +471,12 @@ public class VpnTest {
order.verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(entireUser)); order.verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(entireUser));
// When a new VPN package is set the rules should change to cover that package. // 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(false), aryEq(entireUser));
order.verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(exceptPkg0)); order.verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(exceptPkg0));
// When that VPN package is unset, everything should be undone again in reverse. // 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(false), aryEq(exceptPkg0));
order.verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(entireUser)); order.verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(entireUser));
} }
@@ -631,6 +638,185 @@ public class VpnTest {
assertTrue(caps.hasCapability(NET_CAPABILITY_NOT_CONGESTED)); 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. * Mock some methods of vpn object.
*/ */