Lock down networking when waiting for always-on
Fix: 26694104
Fix: 27042309
Fix: 28335277
Change-Id: I47a4c9d2b98235195b1356af3dabf7235870e4fa
diff --git a/core/java/android/os/INetworkManagementService.aidl b/core/java/android/os/INetworkManagementService.aidl
index b546da0..36ba696 100644
--- a/core/java/android/os/INetworkManagementService.aidl
+++ b/core/java/android/os/INetworkManagementService.aidl
@@ -436,4 +436,6 @@
void addInterfaceToLocalNetwork(String iface, in List<RouteInfo> routes);
void removeInterfaceFromLocalNetwork(String iface);
+
+ void setAllowOnlyVpnForUids(boolean enable, in UidRange[] uidRanges);
}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 370cd57..dd27528 100755
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -4672,6 +4672,14 @@
public static final String ALWAYS_ON_VPN_APP = "always_on_vpn_app";
/**
+ * Whether to block networking outside of VPN connections while always-on is set.
+ * @see #ALWAYS_ON_VPN_APP
+ *
+ * @hide
+ */
+ public static final String ALWAYS_ON_VPN_LOCKDOWN = "always_on_vpn_lockdown";
+
+ /**
* Whether applications can be installed for this user via the system's
* {@link Intent#ACTION_INSTALL_PACKAGE} mechanism.
*
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 8763b93..707b142 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -918,6 +918,13 @@
final boolean networkMetered;
final int uidRules;
+ synchronized (mVpns) {
+ final Vpn vpn = mVpns.get(UserHandle.getUserId(uid));
+ if (vpn != null && vpn.isBlockingUid(uid)) {
+ return true;
+ }
+ }
+
final String iface = (lp == null ? "" : lp.getInterfaceName());
synchronized (mRulesLock) {
networkMetered = mMeteredIfaces.contains(iface);
@@ -3374,23 +3381,42 @@
}
/**
- * Sets up or tears down the always-on VPN for user {@param user} as appropriate.
+ * Starts the always-on VPN {@link VpnService} for user {@param userId}, which should perform
+ * some setup and then call {@code establish()} to connect.
*
- * @return {@code false} in case of errors; {@code true} otherwise.
+ * @return {@code true} if the service was started, the service was already connected, or there
+ * was no always-on VPN to start. {@code false} otherwise.
*/
- private boolean updateAlwaysOnVpn(int user) {
- final String lockdownPackage = getAlwaysOnVpnPackage(user);
- if (lockdownPackage == null) {
- return true;
+ private boolean startAlwaysOnVpn(int userId) {
+ final String alwaysOnPackage;
+ synchronized (mVpns) {
+ Vpn vpn = mVpns.get(userId);
+ if (vpn == null) {
+ // Shouldn't happen as all codepaths that point here should have checked the Vpn
+ // exists already.
+ Slog.wtf(TAG, "User " + userId + " has no Vpn configuration");
+ return false;
+ }
+ alwaysOnPackage = vpn.getAlwaysOnPackage();
+ // Skip if there is no service to start.
+ if (alwaysOnPackage == null) {
+ return true;
+ }
+ // Skip if the service is already established. This isn't bulletproof: it's not bound
+ // until after establish(), so if it's mid-setup onStartCommand will be sent twice,
+ // which may restart the connection.
+ if (vpn.getNetworkInfo().isConnected()) {
+ return true;
+ }
}
- // Create an intent to start the VPN service declared in the app's manifest.
+ // Start the VPN service declared in the app's manifest.
Intent serviceIntent = new Intent(VpnConfig.SERVICE_INTERFACE);
- serviceIntent.setPackage(lockdownPackage);
-
+ serviceIntent.setPackage(alwaysOnPackage);
try {
- return mContext.startServiceAsUser(serviceIntent, UserHandle.of(user)) != null;
+ return mContext.startServiceAsUser(serviceIntent, UserHandle.of(userId)) != null;
} catch (RuntimeException e) {
+ Slog.w(TAG, "VpnService " + serviceIntent + " failed to start", e);
return false;
}
}
@@ -3405,25 +3431,35 @@
return false;
}
- // If the current VPN package is the same as the new one, this is a no-op
- final String oldPackage = getAlwaysOnVpnPackage(userId);
- if (TextUtils.equals(oldPackage, packageName)) {
- return true;
- }
-
synchronized (mVpns) {
Vpn vpn = mVpns.get(userId);
if (vpn == null) {
Slog.w(TAG, "User " + userId + " has no Vpn configuration");
return false;
}
- if (!vpn.setAlwaysOnPackage(packageName)) {
+ // If the current VPN package is the same as the new one, this is a no-op
+ if (TextUtils.equals(packageName, vpn.getAlwaysOnPackage())) {
+ return true;
+ }
+ if (!vpn.setAlwaysOnPackage(packageName, lockdown)) {
return false;
}
- if (!updateAlwaysOnVpn(userId)) {
- vpn.setAlwaysOnPackage(null);
+ if (!startAlwaysOnVpn(userId)) {
+ vpn.setAlwaysOnPackage(null, false);
return false;
}
+
+ // Save the configuration
+ final long token = Binder.clearCallingIdentity();
+ try {
+ final ContentResolver cr = mContext.getContentResolver();
+ Settings.Secure.putStringForUser(cr, Settings.Secure.ALWAYS_ON_VPN_APP,
+ packageName, userId);
+ Settings.Secure.putIntForUser(cr, Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN,
+ (lockdown ? 1 : 0), userId);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
}
return true;
}
@@ -3694,11 +3730,18 @@
}
userVpn = new Vpn(mHandler.getLooper(), mContext, mNetd, userId);
mVpns.put(userId, userVpn);
+
+ final ContentResolver cr = mContext.getContentResolver();
+ String alwaysOnPackage = Settings.Secure.getStringForUser(cr,
+ Settings.Secure.ALWAYS_ON_VPN_APP, userId);
+ final boolean alwaysOnLockdown = Settings.Secure.getIntForUser(cr,
+ Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN, /* default */ 0, userId) != 0;
+ if (alwaysOnPackage != null) {
+ userVpn.setAlwaysOnPackage(alwaysOnPackage, alwaysOnLockdown);
+ }
}
if (mUserManager.getUserInfo(userId).isPrimary() && LockdownVpnTracker.isEnabled()) {
updateLockdownVpn();
- } else {
- updateAlwaysOnVpn(userId);
}
}
@@ -3709,6 +3752,7 @@
loge("Stopped user has no VPN");
return;
}
+ userVpn.onUserStopped();
mVpns.delete(userId);
}
}
@@ -3738,7 +3782,7 @@
if (mUserManager.getUserInfo(userId).isPrimary() && LockdownVpnTracker.isEnabled()) {
updateLockdownVpn();
} else {
- updateAlwaysOnVpn(userId);
+ startAlwaysOnVpn(userId);
}
}
diff --git a/services/core/java/com/android/server/NetworkManagementService.java b/services/core/java/com/android/server/NetworkManagementService.java
index e5b301e..8b2d003 100644
--- a/services/core/java/com/android/server/NetworkManagementService.java
+++ b/services/core/java/com/android/server/NetworkManagementService.java
@@ -1845,6 +1845,22 @@
}
@Override
+ public void setAllowOnlyVpnForUids(boolean add, UidRange[] uidRanges)
+ throws ServiceSpecificException {
+ try {
+ mNetdService.networkRejectNonSecureVpn(add, uidRanges);
+ } catch (ServiceSpecificException e) {
+ Log.w(TAG, "setAllowOnlyVpnForUids(" + add + ", " + Arrays.toString(uidRanges) + ")"
+ + ": netd command failed", e);
+ throw e;
+ } catch (RemoteException e) {
+ Log.w(TAG, "setAllowOnlyVpnForUids(" + add + ", " + Arrays.toString(uidRanges) + ")"
+ + ": netd command failed", e);
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+
+ @Override
public void setUidCleartextNetworkPolicy(int uid, int policy) {
if (Binder.getCallingUid() != uid) {
mContext.enforceCallingOrSelfPermission(CONNECTIVITY_INTERNAL, TAG);
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index 8c4e113..32b9429 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -66,7 +66,6 @@
import android.os.SystemService;
import android.os.UserHandle;
import android.os.UserManager;
-import android.provider.Settings;
import android.security.Credentials;
import android.security.KeyStore;
import android.text.TextUtils;
@@ -128,6 +127,19 @@
private final NetworkCapabilities mNetworkCapabilities;
/**
+ * Whether to keep the connection active after rebooting, or upgrading or reinstalling. This
+ * only applies to {@link VpnService} connections.
+ */
+ private boolean mAlwaysOn = false;
+
+ /**
+ * Whether to disable traffic outside of this VPN even when the VPN is not connected. System
+ * apps can still bypass by choosing explicit networks. Has no effect if {@link mAlwaysOn} is
+ * not set.
+ */
+ private boolean mLockdown = false;
+
+ /**
* List of UIDs that are set to use this VPN by default. Normally, every UID in the user is
* added to this set but that can be changed by adding allowed or disallowed applications. It
* is non-null iff the VPN is connected.
@@ -140,6 +152,14 @@
@GuardedBy("this")
private Set<UidRange> mVpnUsers = null;
+ /**
+ * List of UIDs for which networking should be blocked until VPN is ready, during brief periods
+ * when VPN is not running. For example, during system startup or after a crash.
+ * @see mLockdown
+ */
+ @GuardedBy("this")
+ private Set<UidRange> mBlockedUsers = new ArraySet<>();
+
// Handle of user initiating VPN.
private final int mUserHandle;
@@ -194,9 +214,10 @@
* manifest guarded by {@link android.Manifest.permission.BIND_VPN_SERVICE},
* otherwise the call will fail.
*
- * @param newPackage the package to designate as always-on VPN supplier.
+ * @param packageName the package to designate as always-on VPN supplier.
+ * @param lockdown whether to prevent traffic outside of a VPN, for example while connecting.
*/
- public synchronized boolean setAlwaysOnPackage(String packageName) {
+ public synchronized boolean setAlwaysOnPackage(String packageName, boolean lockdown) {
enforceControlPermissionOrInternalCaller();
// Disconnect current VPN.
@@ -210,14 +231,9 @@
prepareInternal(packageName);
}
- // Save the new package name in Settings.Secure.
- final long token = Binder.clearCallingIdentity();
- try {
- Settings.Secure.putStringForUser(mContext.getContentResolver(),
- Settings.Secure.ALWAYS_ON_VPN_APP, packageName, mUserHandle);
- } finally {
- Binder.restoreCallingIdentity(token);
- }
+ mAlwaysOn = (packageName != null);
+ mLockdown = (mAlwaysOn && lockdown);
+ setVpnForcedLocked(mLockdown);
return true;
}
@@ -229,14 +245,7 @@
*/
public synchronized String getAlwaysOnPackage() {
enforceControlPermissionOrInternalCaller();
-
- final long token = Binder.clearCallingIdentity();
- try {
- return Settings.Secure.getStringForUser(mContext.getContentResolver(),
- Settings.Secure.ALWAYS_ON_VPN_APP, mUserHandle);
- } finally {
- Binder.restoreCallingIdentity(token);
- }
+ return (mAlwaysOn ? mPackage : null);
}
/**
@@ -258,6 +267,11 @@
* @return true if the operation is succeeded.
*/
public synchronized boolean prepare(String oldPackage, String newPackage) {
+ // Stop an existing always-on VPN from being dethroned by other apps.
+ if (mAlwaysOn && !TextUtils.equals(mPackage, newPackage)) {
+ return false;
+ }
+
if (oldPackage != null) {
if (getAppUid(oldPackage, mUserHandle) != mOwnerUID) {
// The package doesn't match. We return false (to obtain user consent) unless the
@@ -281,11 +295,6 @@
return true;
}
- // Stop an existing always-on VPN from being dethroned by other apps.
- if (getAlwaysOnPackage() != null) {
- return false;
- }
-
// Check that the caller is authorized.
enforceControlPermission();
@@ -469,7 +478,7 @@
mNetworkInfo.setDetailedState(DetailedState.CONNECTING, null, null);
NetworkMisc networkMisc = new NetworkMisc();
- networkMisc.allowBypass = mConfig.allowBypass;
+ networkMisc.allowBypass = mConfig.allowBypass && !mLockdown;
long token = Binder.clearCallingIdentity();
try {
@@ -685,7 +694,7 @@
final long token = Binder.clearCallingIdentity();
List<UserInfo> users;
try {
- users = UserManager.get(mContext).getUsers();
+ users = UserManager.get(mContext).getUsers(true);
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -774,18 +783,22 @@
public void onUserAdded(int userHandle) {
// If the user is restricted tie them to the parent user's VPN
UserInfo user = UserManager.get(mContext).getUserInfo(userHandle);
- if (user.isRestricted() && user.restrictedProfileParentId == mUserHandle
- && mVpnUsers != null) {
+ if (user.isRestricted() && user.restrictedProfileParentId == mUserHandle) {
synchronized(Vpn.this) {
- try {
- addUserToRanges(mVpnUsers, userHandle, mConfig.allowedApplications,
- mConfig.disallowedApplications);
- if (mNetworkAgent != null) {
- final List<UidRange> ranges = uidRangesForUser(userHandle);
- mNetworkAgent.addUidRanges(ranges.toArray(new UidRange[ranges.size()]));
+ if (mVpnUsers != null) {
+ try {
+ addUserToRanges(mVpnUsers, userHandle, mConfig.allowedApplications,
+ mConfig.disallowedApplications);
+ if (mNetworkAgent != null) {
+ final List<UidRange> ranges = uidRangesForUser(userHandle);
+ mNetworkAgent.addUidRanges(ranges.toArray(new UidRange[ranges.size()]));
+ }
+ } catch (Exception e) {
+ Log.wtf(TAG, "Failed to add restricted user to owner", e);
}
- } catch (Exception e) {
- Log.wtf(TAG, "Failed to add restricted user to owner", e);
+ }
+ if (mAlwaysOn) {
+ setVpnForcedLocked(mLockdown);
}
}
}
@@ -794,19 +807,101 @@
public void onUserRemoved(int userHandle) {
// clean up if restricted
UserInfo user = UserManager.get(mContext).getUserInfo(userHandle);
- if (user.isRestricted() && user.restrictedProfileParentId == mUserHandle
- && mVpnUsers != null) {
+ if (user.isRestricted() && user.restrictedProfileParentId == mUserHandle) {
synchronized(Vpn.this) {
- try {
- removeVpnUserLocked(userHandle);
- } catch (Exception e) {
- Log.wtf(TAG, "Failed to remove restricted user to owner", e);
+ if (mVpnUsers != null) {
+ try {
+ removeVpnUserLocked(userHandle);
+ } catch (Exception e) {
+ Log.wtf(TAG, "Failed to remove restricted user to owner", e);
+ }
+ }
+ if (mAlwaysOn) {
+ setVpnForcedLocked(mLockdown);
}
}
}
}
/**
+ * Called when the user associated with this VPN has just been stopped.
+ */
+ public synchronized void onUserStopped() {
+ // Switch off networking lockdown (if it was enabled)
+ setVpnForcedLocked(false);
+ mAlwaysOn = false;
+
+ // Quit any active connections
+ agentDisconnect();
+ }
+
+ /**
+ * Restrict network access from all UIDs affected by this {@link Vpn}, apart from the VPN
+ * service app itself, to only sockets that have had {@code protect()} called on them. All
+ * non-VPN traffic is blocked via a {@code PROHIBIT} response from the kernel.
+ *
+ * The exception for the VPN UID isn't technically necessary -- setup should use protected
+ * sockets -- but in practice it saves apps that don't protect their sockets from breaking.
+ *
+ * Calling multiple times with {@param enforce} = {@code true} will recreate the set of UIDs to
+ * block every time, and if anything has changed update using {@link #setAllowOnlyVpnForUids}.
+ *
+ * @param enforce {@code true} to require that all traffic under the jurisdiction of this
+ * {@link Vpn} goes through a VPN connection or is blocked until one is
+ * available, {@code false} to lift the requirement.
+ *
+ * @see #mBlockedUsers
+ */
+ @GuardedBy("this")
+ private void setVpnForcedLocked(boolean enforce) {
+ final Set<UidRange> removedRanges = new ArraySet<>(mBlockedUsers);
+ if (enforce) {
+ final Set<UidRange> addedRanges = createUserAndRestrictedProfilesRanges(mUserHandle,
+ /* allowedApplications */ null,
+ /* disallowedApplications */ Collections.singletonList(mPackage));
+
+ removedRanges.removeAll(addedRanges);
+ addedRanges.removeAll(mBlockedUsers);
+
+ setAllowOnlyVpnForUids(false, removedRanges);
+ setAllowOnlyVpnForUids(true, addedRanges);
+ } else {
+ setAllowOnlyVpnForUids(false, removedRanges);
+ }
+ }
+
+ /**
+ * Either add or remove a list of {@link UidRange}s to the list of UIDs that are only allowed
+ * to make connections through sockets that have had {@code protect()} called on them.
+ *
+ * @param enforce {@code true} to add to the blacklist, {@code false} to remove.
+ * @param ranges {@link Collection} of {@link UidRange}s to add (if {@param enforce} is
+ * {@code true}) or to remove.
+ * @return {@code true} if all of the UIDs were added/removed. {@code false} otherwise,
+ * including added ranges that already existed or removed ones that didn't.
+ */
+ @GuardedBy("this")
+ private boolean setAllowOnlyVpnForUids(boolean enforce, Collection<UidRange> ranges) {
+ if (ranges.size() == 0) {
+ return true;
+ }
+ final UidRange[] rangesArray = ranges.toArray(new UidRange[ranges.size()]);
+ try {
+ mNetd.setAllowOnlyVpnForUids(enforce, rangesArray);
+ } catch (RemoteException | RuntimeException e) {
+ Log.e(TAG, "Updating blocked=" + enforce
+ + " for UIDs " + Arrays.toString(ranges.toArray()) + " failed", e);
+ return false;
+ }
+ if (enforce) {
+ mBlockedUsers.addAll(ranges);
+ } else {
+ mBlockedUsers.removeAll(ranges);
+ }
+ return true;
+ }
+
+ /**
* Return the configuration of the currently running VPN.
*/
public VpnConfig getVpnConfig() {
@@ -959,6 +1054,21 @@
return false;
}
+ /**
+ * @return {@code true} if the set of users blocked whilst waiting for VPN to connect includes
+ * the UID {@param uid}, {@code false} otherwise.
+ *
+ * @see #mBlockedUsers
+ */
+ public synchronized boolean isBlockingUid(int uid) {
+ for (UidRange uidRange : mBlockedUsers) {
+ if (uidRange.contains(uid)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private native int jniCreate(int mtu);
private native String jniGetName(int tun);
private native int jniSetAddresses(String interfaze, String addresses);
diff --git a/services/tests/servicestests/src/com/android/server/connectivity/VpnTest.java b/services/tests/servicestests/src/com/android/server/connectivity/VpnTest.java
index 3295bf5..5d8b843 100644
--- a/services/tests/servicestests/src/com/android/server/connectivity/VpnTest.java
+++ b/services/tests/servicestests/src/com/android/server/connectivity/VpnTest.java
@@ -20,9 +20,11 @@
import static android.content.pm.UserInfo.FLAG_MANAGED_PROFILE;
import static android.content.pm.UserInfo.FLAG_PRIMARY;
import static android.content.pm.UserInfo.FLAG_RESTRICTED;
+import static org.mockito.AdditionalMatchers.*;
import static org.mockito.Mockito.*;
import android.annotation.UserIdInt;
+import android.app.AppOpsManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
@@ -65,16 +67,35 @@
managedProfileA.profileGroupId = primaryUser.id;
}
+ /**
+ * Names and UIDs for some fake packages. Important points:
+ * - UID is ordered increasing.
+ * - One pair of packages have consecutive UIDs.
+ */
+ static final String[] PKGS = {"com.example", "org.example", "net.example", "web.vpn"};
+ static final int[] PKG_UIDS = {66, 77, 78, 400};
+
+ // Mock packages
+ static final Map<String, Integer> mPackages = new ArrayMap<>();
+ static {
+ for (int i = 0; i < PKGS.length; i++) {
+ mPackages.put(PKGS[i], PKG_UIDS[i]);
+ }
+ }
+
@Mock private Context mContext;
@Mock private UserManager mUserManager;
@Mock private PackageManager mPackageManager;
@Mock private INetworkManagementService mNetService;
+ @Mock private AppOpsManager mAppOps;
@Override
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
when(mContext.getPackageManager()).thenReturn(mPackageManager);
+ setMockedPackages(mPackages);
when(mContext.getSystemService(eq(Context.USER_SERVICE))).thenReturn(mUserManager);
+ when(mContext.getSystemService(eq(Context.APP_OPS_SERVICE))).thenReturn(mAppOps);
doNothing().when(mNetService).registerObserver(any());
}
@@ -82,7 +103,7 @@
public void testRestrictedProfilesAreAddedToVpn() {
setMockedUsers(primaryUser, secondaryUser, restrictedProfileA, restrictedProfileB);
- final Vpn vpn = createVpn(primaryUser.id);
+ final Vpn vpn = new MockVpn(primaryUser.id);
final Set<UidRange> ranges = vpn.createUserAndRestrictedProfilesRanges(primaryUser.id,
null, null);
@@ -96,7 +117,7 @@
public void testManagedProfilesAreNotAddedToVpn() {
setMockedUsers(primaryUser, managedProfileA);
- final Vpn vpn = createVpn(primaryUser.id);
+ final Vpn vpn = new MockVpn(primaryUser.id);
final Set<UidRange> ranges = vpn.createUserAndRestrictedProfilesRanges(primaryUser.id,
null, null);
@@ -109,7 +130,7 @@
public void testAddUserToVpnOnlyAddsOneUser() {
setMockedUsers(primaryUser, restrictedProfileA, managedProfileA);
- final Vpn vpn = createVpn(primaryUser.id);
+ final Vpn vpn = new MockVpn(primaryUser.id);
final Set<UidRange> ranges = new ArraySet<>();
vpn.addUserToRanges(ranges, primaryUser.id, null, null);
@@ -120,42 +141,123 @@
@SmallTest
public void testUidWhiteAndBlacklist() throws Exception {
- final Map<String, Integer> packages = new ArrayMap<>();
- packages.put("com.example", 66);
- packages.put("org.example", 77);
- packages.put("net.example", 78);
- setMockedPackages(packages);
-
- final Vpn vpn = createVpn(primaryUser.id);
+ final Vpn vpn = new MockVpn(primaryUser.id);
final UidRange user = UidRange.createForUser(primaryUser.id);
+ final String[] packages = {PKGS[0], PKGS[1], PKGS[2]};
// Whitelist
final Set<UidRange> allow = vpn.createUserAndRestrictedProfilesRanges(primaryUser.id,
- new ArrayList<String>(packages.keySet()), null);
+ Arrays.asList(packages), null);
assertEquals(new ArraySet<>(Arrays.asList(new UidRange[] {
- new UidRange(user.start + 66, user.start + 66),
- new UidRange(user.start + 77, user.start + 78)
+ new UidRange(user.start + PKG_UIDS[0], user.start + PKG_UIDS[0]),
+ new UidRange(user.start + PKG_UIDS[1], user.start + PKG_UIDS[2])
})), allow);
// Blacklist
final Set<UidRange> disallow = vpn.createUserAndRestrictedProfilesRanges(primaryUser.id,
- null, new ArrayList<String>(packages.keySet()));
+ null, Arrays.asList(packages));
assertEquals(new ArraySet<>(Arrays.asList(new UidRange[] {
- new UidRange(user.start, user.start + 65),
- new UidRange(user.start + 67, user.start + 76),
- new UidRange(user.start + 79, user.stop)
+ new UidRange(user.start, user.start + PKG_UIDS[0] - 1),
+ new UidRange(user.start + PKG_UIDS[0] + 1, user.start + PKG_UIDS[1] - 1),
+ /* Empty range between UIDS[1] and UIDS[2], should be excluded, */
+ new UidRange(user.start + PKG_UIDS[2] + 1, user.stop)
})), disallow);
}
+ @SmallTest
+ public void testLockdownChangingPackage() throws Exception {
+ final MockVpn vpn = new MockVpn(primaryUser.id);
+ final UidRange user = UidRange.createForUser(primaryUser.id);
+
+ // Default state.
+ vpn.assertUnblocked(user.start + PKG_UIDS[0], user.start + PKG_UIDS[1], user.start + PKG_UIDS[2], user.start + PKG_UIDS[3]);
+
+ // Set always-on without lockdown.
+ assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false));
+ vpn.assertUnblocked(user.start + PKG_UIDS[0], user.start + PKG_UIDS[1], user.start + PKG_UIDS[2], user.start + PKG_UIDS[3]);
+
+ // Set always-on with lockdown.
+ assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true));
+ verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(new UidRange[] {
+ new UidRange(user.start, user.start + PKG_UIDS[1] - 1),
+ new UidRange(user.start + PKG_UIDS[1] + 1, user.stop)
+ }));
+ vpn.assertBlocked(user.start + PKG_UIDS[0], user.start + PKG_UIDS[2], user.start + PKG_UIDS[3]);
+ vpn.assertUnblocked(user.start + PKG_UIDS[1]);
+
+ // Switch to another app.
+ assertTrue(vpn.setAlwaysOnPackage(PKGS[3], true));
+ verify(mNetService).setAllowOnlyVpnForUids(eq(false), aryEq(new UidRange[] {
+ new UidRange(user.start, user.start + PKG_UIDS[1] - 1),
+ new UidRange(user.start + PKG_UIDS[1] + 1, user.stop)
+ }));
+ verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(new UidRange[] {
+ new UidRange(user.start, user.start + PKG_UIDS[3] - 1),
+ new UidRange(user.start + PKG_UIDS[3] + 1, user.stop)
+ }));
+ vpn.assertBlocked(user.start + PKG_UIDS[0], user.start + PKG_UIDS[1], user.start + PKG_UIDS[2]);
+ vpn.assertUnblocked(user.start + PKG_UIDS[3]);
+ }
+
+ @SmallTest
+ public void testLockdownAddingAProfile() throws Exception {
+ final MockVpn vpn = new MockVpn(primaryUser.id);
+ setMockedUsers(primaryUser);
+
+ // Make a copy of the restricted profile, as we're going to mark it deleted halfway through.
+ final UserInfo tempProfile = new UserInfo(restrictedProfileA.id, restrictedProfileA.name,
+ restrictedProfileA.flags);
+ tempProfile.restrictedProfileParentId = primaryUser.id;
+
+ final UidRange user = UidRange.createForUser(primaryUser.id);
+ final UidRange profile = UidRange.createForUser(tempProfile.id);
+
+ // Set lockdown.
+ assertTrue(vpn.setAlwaysOnPackage(PKGS[3], true));
+ verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(new UidRange[] {
+ new UidRange(user.start, user.start + PKG_UIDS[3] - 1),
+ new UidRange(user.start + PKG_UIDS[3] + 1, user.stop)
+ }));
+
+ // Verify restricted user isn't affected at first.
+ vpn.assertUnblocked(profile.start + PKG_UIDS[0]);
+
+ // Add the restricted user.
+ setMockedUsers(primaryUser, tempProfile);
+ vpn.onUserAdded(tempProfile.id);
+ verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(new UidRange[] {
+ new UidRange(profile.start, profile.start + PKG_UIDS[3] - 1),
+ new UidRange(profile.start + PKG_UIDS[3] + 1, profile.stop)
+ }));
+
+ // Remove the restricted user.
+ tempProfile.partial = true;
+ vpn.onUserRemoved(tempProfile.id);
+ verify(mNetService).setAllowOnlyVpnForUids(eq(false), aryEq(new UidRange[] {
+ new UidRange(profile.start, profile.start + PKG_UIDS[3] - 1),
+ new UidRange(profile.start + PKG_UIDS[3] + 1, profile.stop)
+ }));
+ }
+
/**
- * @return A subclass of {@link Vpn} which is reliably:
- * <ul>
- * <li>Associated with a specific user ID</li>
- * <li>Not in always-on mode</li>
- * </ul>
+ * A subclass of {@link Vpn} with some of the fields pre-mocked.
*/
- private Vpn createVpn(@UserIdInt int userId) {
- return new Vpn(Looper.myLooper(), mContext, mNetService, userId);
+ private class MockVpn extends Vpn {
+ public MockVpn(@UserIdInt int userId) {
+ super(Looper.myLooper(), mContext, mNetService, userId);
+ }
+
+ public void assertBlocked(int... uids) {
+ for (int uid : uids) {
+ assertTrue("Uid " + uid + " should be blocked", isBlockingUid(uid));
+ }
+ }
+
+ public void assertUnblocked(int... uids) {
+ for (int uid : uids) {
+ assertFalse("Uid " + uid + " should not be blocked", isBlockingUid(uid));
+ }
+ }
}
/**
@@ -167,9 +269,19 @@
userMap.put(user.id, user);
}
+ /**
+ * @see UserManagerService#getUsers(boolean)
+ */
doAnswer(invocation -> {
- return new ArrayList(userMap.values());
- }).when(mUserManager).getUsers();
+ final boolean excludeDying = (boolean) invocation.getArguments()[0];
+ final ArrayList<UserInfo> result = new ArrayList<>(users.length);
+ for (UserInfo ui : users) {
+ if (!excludeDying || (ui.isEnabled() && !ui.partial)) {
+ result.add(ui);
+ }
+ }
+ return result;
+ }).when(mUserManager).getUsers(anyBoolean());
doAnswer(invocation -> {
final int id = (int) invocation.getArguments()[0];